]> git.basschouten.com Git - openhab-addons.git/blob
ed3d3d10d9103f68bff043be8e750f2c285859a7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.rrd4j.internal;
14
15 import java.io.IOException;
16 import java.nio.file.Files;
17 import java.nio.file.Path;
18 import java.time.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;
27 import java.util.Map;
28 import java.util.Objects;
29 import java.util.Set;
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;
40
41 import javax.measure.Quantity;
42 import javax.measure.Unit;
43
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;
92
93 /**
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>.
97  *
98  * @author Kai Kreuzer - Initial contribution
99  * @author Jan N. Klug - some improvements
100  * @author Karel Goderis - remove TimerThread dependency
101  */
102 @NonNullByDefault
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 {
107
108     private record Key(long timestamp, String name) implements Comparable<Key> {
109         @Override
110         public int compareTo(Key other) {
111             int c = Long.compare(timestamp, other.timestamp);
112
113             return (c == 0) ? Objects.compare(name, other.name, String::compareTo) : c;
114         }
115     }
116
117     public static final String SERVICE_ID = "rrd4j";
118
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";
122
123     private static final Set<String> SUPPORTED_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT,
124             CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.COLOR);
125
126     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
127             new NamedThreadFactory("RRD4j"));
128
129     private final Map<String, RrdDefConfig> rrdDefs = new ConcurrentHashMap<>();
130
131     private final ConcurrentSkipListMap<Key, Double> storageMap = new ConcurrentSkipListMap<>(Key::compareTo);
132
133     private static final String DATASOURCE_STATE = "state";
134
135     private static final Path DB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "persistence", "rrd4j").toAbsolutePath();
136
137     private static final RrdDbPool DATABASE_POOL = new RrdDbPool();
138
139     private final Logger logger = LoggerFactory.getLogger(RRD4jPersistenceService.class);
140     private final ItemRegistry itemRegistry;
141     private boolean active = false;
142
143     public static Path getDatabasePath(String name) {
144         return DB_FOLDER.resolve(name + ".rrd");
145     }
146
147     public static RrdDbPool getDatabasePool() {
148         return DATABASE_POOL;
149     }
150
151     private final ScheduledFuture<?> storeJob;
152
153     @Activate
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);
157         modified(config);
158         active = true;
159     }
160
161     @Modified
162     protected void modified(final Map<String, Object> config) {
163         // clean existing definitions
164         rrdDefs.clear();
165
166         // add default configurations
167
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
177         defaultNumeric
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);
180
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);
193
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);
204
205         if (config.isEmpty()) {
206             logger.debug("using default configuration only");
207             return;
208         }
209
210         Iterator<String> keys = config.keySet().iterator();
211         while (keys.hasNext()) {
212             String key = keys.next();
213
214             if ("service.pid".equals(key) || "component.name".equals(key)) {
215                 // ignore service.pid and name
216                 continue;
217             }
218
219             String[] subkeys = key.split("\\.");
220             if (subkeys.length != 2) {
221                 logger.debug("config '{}' should have the format 'name.configkey'", key);
222                 continue;
223             }
224
225             Object v = config.get(key);
226             if (v instanceof String value) {
227                 String name = subkeys[0].toLowerCase();
228                 String property = subkeys[1].toLowerCase();
229
230                 if (value.isBlank()) {
231                     logger.trace("Config is empty: {}", property);
232                     continue;
233                 } else {
234                     logger.trace("Processing config: {} = {}", property, value);
235                 }
236
237                 RrdDefConfig rrdDef = rrdDefs.get(name);
238                 if (rrdDef == null) {
239                     rrdDef = new RrdDefConfig(name);
240                     rrdDefs.put(name, rrdDef);
241                 }
242
243                 try {
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);
250                     } else {
251                         logger.debug("Unknown property {} : {}", property, value);
252                     }
253                 } catch (IllegalArgumentException e) {
254                     logger.warn("Ignoring illegal configuration: {}", e.getMessage());
255                 }
256             }
257         }
258
259         for (RrdDefConfig rrdDef : rrdDefs.values()) {
260             if (rrdDef.isValid()) {
261                 logger.debug("Created {}", rrdDef);
262             } else {
263                 logger.info("Removing invalid definition {}", rrdDef);
264                 rrdDefs.remove(rrdDef.name);
265             }
266         }
267     }
268
269     @Deactivate
270     protected void deactivate() {
271         active = false;
272         storeJob.cancel(false);
273
274         // make sure we really store everything
275         doStore(true);
276     }
277
278     @Override
279     public String getId() {
280         return SERVICE_ID;
281     }
282
283     @Override
284     public String getLabel(@Nullable Locale locale) {
285         return "RRD4j";
286     }
287
288     @Override
289     public void store(final Item item, @Nullable final String alias) {
290         if (!active) {
291             logger.warn("Tried to store {} but service is not yet ready (or shutting down).", item);
292             return;
293         }
294
295         if (!isSupportedItemType(item)) {
296             logger.trace("Ignoring item '{}' since its type {} is not supported", item.getName(), item.getType());
297             return;
298         }
299         final String name = alias == null ? item.getName() : alias;
300
301         Double value;
302
303         if (item instanceof NumberItem nItem && item.getState() instanceof QuantityType<?> qState) {
304             Unit<? extends Quantity<?>> unit = nItem.getUnit();
305             if (unit != null) {
306                 QuantityType<?> convertedState = qState.toUnit(unit);
307                 if (convertedState != null) {
308                     value = convertedState.doubleValue();
309                 } else {
310                     value = null;
311                     logger.warn(
312                             "Failed to convert state '{}' to unit '{}'. Please check your item definition for correctness.",
313                             qState, unit);
314                 }
315             } else {
316                 value = qState.doubleValue();
317             }
318         } else {
319             DecimalType state = item.getStateAs(DecimalType.class);
320             if (state != null) {
321                 value = state.toBigDecimal().doubleValue();
322             } else {
323                 value = null;
324             }
325         }
326
327         if (value == null) {
328             // we could not convert the value
329             return;
330         }
331
332         long now = System.currentTimeMillis() / 1000;
333         Double oldValue = storageMap.put(new Key(now, name), value);
334         if (oldValue != null && !oldValue.equals(value)) {
335             logger.debug(
336                     "Discarding value {} for item {} with timestamp {} because a new value ({}) arrived with the same timestamp.",
337                     oldValue, name, now, value);
338         }
339     }
340
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);
350             } else {
351                 return;
352             }
353         }
354     }
355
356     private synchronized void writePointToDatabase(String name, double value, long timestamp) {
357         RrdDb db = null;
358         try {
359             db = getDB(name, true);
360         } catch (Exception e) {
361             logger.warn("Failed to open rrd4j database '{}' to store data ({})", name, e.toString());
362         }
363         if (db == null) {
364             return;
365         }
366
367         ConsolFun function = getConsolidationFunction(db);
368         if (function != ConsolFun.AVERAGE) {
369             try {
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)) {
377                         Sample sample = db.createSample();
378                         sample.setTime(timestamp - 1);
379                         sample.setValue(DATASOURCE_STATE, lastValue);
380                         sample.update();
381                         logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database (again)", name,
382                                 lastValue, timestamp - 1);
383                     }
384                 }
385             } catch (IOException e) {
386                 logger.debug("Error storing last value (again) for {}: {}", e.getMessage(), name);
387             }
388         }
389         try {
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();
396             }
397             sample.setValue(DATASOURCE_STATE, storeValue);
398             sample.update();
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());
402         }
403         try {
404             db.close();
405         } catch (IOException e) {
406             logger.debug("Error closing rrd4j database: {}", e.getMessage());
407         }
408     }
409
410     @Override
411     public void store(Item item) {
412         store(item, null);
413     }
414
415     @Override
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 + ")");
421         }
422
423         String itemName = filter.getItemName();
424         if (itemName == null) {
425             logger.warn("Item name is missing in filter {}", filter);
426             return List.of();
427         }
428         logger.trace("Querying rrd4j database for item '{}'", itemName);
429
430         RrdDb db = null;
431         try {
432             db = getDB(itemName, false);
433         } catch (Exception e) {
434             logger.warn("Failed to open rrd4j database '{}' for querying ({})", itemName, e.toString());
435             return List.of();
436         }
437         if (db == null) {
438             logger.debug("Could not find item '{}' in rrd4j database", itemName);
439             return List.of();
440         }
441
442         Item item = null;
443         Unit<?> unit = null;
444         try {
445             item = itemRegistry.getItem(itemName);
446             if (item instanceof GroupItem groupItem) {
447                 item = groupItem.getBaseItem();
448             }
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();
453             }
454         } catch (ItemNotFoundException e) {
455             logger.debug("Could not find item '{}' in registry", itemName);
456         }
457
458         long start = 0L;
459         long end = filterEndDate == null ? System.currentTimeMillis() / 1000
460                 : filterEndDate.toInstant().getEpochSecond();
461
462         DoubleFunction<State> toState = toStateMapper(item, unit);
463
464         try {
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);
480                         } else {
481                             return List.of();
482                         }
483                     } else {
484                         start = end;
485                     }
486                 } else {
487                     throw new UnsupportedOperationException(
488                             "rrd4j does not allow querys without a begin date, unless order is descending and a single value is requested");
489                 }
490             } else {
491                 start = filterBeginDate.toInstant().getEpochSecond();
492             }
493
494             // do not call method {@link RrdDb#createFetchRequest(ConsolFun, long, long, long)} if start > end to avoid
495             // an IAE to be thrown
496             if (start > end) {
497                 logger.debug("Could not query rrd4j database for item '{}': start ({}) > end ({})", itemName, start,
498                         end);
499                 return List.of();
500             }
501
502             FetchRequest request = db.createFetchRequest(getConsolidationFunction(db), start, end, 1);
503             FetchData result = request.fetchData();
504
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;
509
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))) {
514                     State state;
515
516                     if (prevValue == value) {
517                         state = prevState;
518                     } else {
519                         prevState = state = toState.apply(value);
520                         prevValue = value;
521                     }
522
523                     RRD4jItem rrd4jItem = new RRD4jItem(itemName, state, zdt);
524                     items.add(rrd4jItem);
525                 }
526                 zdt = zdt.plusSeconds(step);
527                 ts += step;
528             }
529             return items;
530         } catch (IOException e) {
531             logger.warn("Could not query rrd4j database for item '{}': {}", itemName, e.getMessage());
532             return List.of();
533         } finally {
534             try {
535                 db.close();
536             } catch (IOException e) {
537                 logger.debug("Error closing rrd4j database: {}", e.getMessage());
538             }
539         }
540     }
541
542     @Override
543     public Set<PersistenceItemInfo> getItemInfo() {
544         return Set.of();
545     }
546
547     protected synchronized @Nullable RrdDb getDB(String alias, boolean createFileIfAbsent) {
548         RrdDb db = null;
549         Path path = getDatabasePath(alias);
550         try {
551             Builder builder = RrdDb.getBuilder();
552             builder.setPool(DATABASE_POOL);
553
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);
561                 }
562                 RrdDef rrdDef = getRrdDef(alias, path);
563                 if (rrdDef != null) {
564                     // create a new database file
565                     builder.setRrdDef(rrdDef);
566                     db = builder.build();
567                 } else {
568                     logger.debug(
569                             "Did not create rrd4j database for item '{}' since no rrd definition could be determined. This is likely due to an unsupported item type.",
570                             alias);
571                 }
572             }
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());
578         }
579         return db;
580     }
581
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)) {
588                 useRdc = rdc;
589                 break;
590             }
591         }
592         if (useRdc == null) { // not defined, use defaults
593             try {
594                 Item item = itemRegistry.getItem(itemName);
595                 if (!isSupportedItemType(item)) {
596                     return null;
597                 }
598                 if (item instanceof NumberItem numberItem) {
599                     useRdc = numberItem.getDimension() != null ? rrdDefs.get(DEFAULT_QUANTIFIABLE)
600                             : rrdDefs.get(DEFAULT_NUMERIC);
601                 } else {
602                     useRdc = rrdDefs.get(DEFAULT_OTHER);
603                 }
604             } catch (ItemNotFoundException e) {
605                 logger.debug("Could not find item '{}' in registry", itemName);
606                 return null;
607             }
608         }
609         logger.trace("Using rrd definition '{}' for item '{}'.", useRdc, itemName);
610         return useRdc;
611     }
612
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);
622             }
623             return rrdDef;
624         } else {
625             return null;
626         }
627     }
628
629     public ConsolFun getConsolidationFunction(RrdDb db) {
630         try {
631             return db.getRrdDef().getArcDefs()[0].getConsolFun();
632         } catch (IOException e) {
633             return ConsolFun.MAX;
634         }
635     }
636
637     /**
638      * Get the state Mapper for a given item
639      *
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
643      */
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) {
653             if (unit != null) {
654                 return (value) -> new QuantityType<>(value, unit);
655             }
656         }
657         return DecimalType::new;
658     }
659
660     private boolean isSupportedItemType(Item item) {
661         if (item instanceof GroupItem groupItem) {
662             final Item baseItem = groupItem.getBaseItem();
663             if (baseItem != null) {
664                 item = baseItem;
665             }
666         }
667
668         return SUPPORTED_TYPES.contains(ItemUtil.getMainItemType(item.getType()));
669     }
670
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) {
676             return List.of();
677         }
678     }
679
680     private static class RrdArchiveDef {
681         public @Nullable ConsolFun fcn;
682         public double xff;
683         public int steps, rows;
684
685         @Override
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();
692         }
693     }
694
695     private class RrdDefConfig {
696         public String name;
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;
702
703         private boolean isInitialized;
704
705         public RrdDefConfig(String name) {
706             this.name = name;
707             archives = new ArrayList<>();
708             itemNames = new ArrayList<>();
709             isInitialized = false;
710         }
711
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);
716                 return;
717             }
718
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;
727             } else {
728                 logger.warn("{}: dsType {} not supported", name, opts[0]);
729             }
730
731             heartbeat = Integer.parseInt(opts[1]);
732
733             if ("U".equals(opts[2])) {
734                 min = Double.NaN;
735             } else {
736                 min = Double.parseDouble(opts[2]);
737             }
738
739             if ("U".equals(opts[3])) {
740                 max = Double.NaN;
741             } else {
742                 max = Double.parseDouble(opts[3]);
743             }
744
745             step = Integer.parseInt(opts[4]);
746
747             isInitialized = true; // successfully initialized
748
749             return;
750         }
751
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);
758                     return;
759                 }
760                 RrdArchiveDef arc = new RrdArchiveDef();
761
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;
774                 } else {
775                     logger.warn("{}: consolidation function  {} not supported", name, opts[0]);
776                 }
777                 arc.xff = Double.parseDouble(opts[1]);
778                 arc.steps = Integer.parseInt(opts[2]);
779                 arc.rows = Integer.parseInt(opts[3]);
780                 archives.add(arc);
781             }
782         }
783
784         public void addItems(String itemsString) {
785             Collections.addAll(itemNames, itemsString.split(","));
786         }
787
788         public boolean appliesTo(String item) {
789             return itemNames.contains(item);
790         }
791
792         public boolean isValid() { // a valid configuration must be initialized
793             // and contain at least one function
794             return isInitialized && !archives.isEmpty();
795         }
796
797         @Override
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());
807             }
808             sb.append("] ");
809             sb.append(itemNames.size()).append(" items(s) = [");
810             for (String item : itemNames) {
811                 sb.append(item).append(" ");
812             }
813             sb.append("]");
814             return sb.toString();
815         }
816     }
817
818     @Override
819     public List<PersistenceStrategy> getDefaultStrategies() {
820         return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE,
821                 new PersistenceCronStrategy("everyMinute", "0 * * * * ?"));
822     }
823 }