]> git.basschouten.com Git - openhab-addons.git/blob
f891231ff28beaf187f62f48c25fe196a2bb5bf7
[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.service.component.annotations.Activate;
75 import org.osgi.service.component.annotations.Component;
76 import org.osgi.service.component.annotations.ConfigurationPolicy;
77 import org.osgi.service.component.annotations.Deactivate;
78 import org.osgi.service.component.annotations.Modified;
79 import org.osgi.service.component.annotations.Reference;
80 import org.rrd4j.ConsolFun;
81 import org.rrd4j.DsType;
82 import org.rrd4j.core.FetchData;
83 import org.rrd4j.core.FetchRequest;
84 import org.rrd4j.core.RrdDb;
85 import org.rrd4j.core.RrdDb.Builder;
86 import org.rrd4j.core.RrdDbPool;
87 import org.rrd4j.core.RrdDef;
88 import org.rrd4j.core.Sample;
89 import org.slf4j.Logger;
90 import org.slf4j.LoggerFactory;
91
92 /**
93  * This is the implementation of the RRD4j {@link PersistenceService}. To learn
94  * more about RRD4j please visit their
95  * <a href="https://github.com/rrd4j/rrd4j">website</a>.
96  *
97  * @author Kai Kreuzer - Initial contribution
98  * @author Jan N. Klug - some improvements
99  * @author Karel Goderis - remove TimerThread dependency
100  */
101 @NonNullByDefault
102 @Component(service = { PersistenceService.class,
103         QueryablePersistenceService.class }, configurationPid = "org.openhab.rrd4j", configurationPolicy = ConfigurationPolicy.OPTIONAL)
104 public class RRD4jPersistenceService implements QueryablePersistenceService {
105
106     private record Key(long timestamp, String name) implements Comparable<Key> {
107         @Override
108         public int compareTo(Key other) {
109             int c = Long.compare(timestamp, other.timestamp);
110
111             return (c == 0) ? Objects.compare(name, other.name, String::compareTo) : c;
112         }
113     }
114
115     public static final String SERVICE_ID = "rrd4j";
116
117     private static final String DEFAULT_OTHER = "default_other";
118     private static final String DEFAULT_NUMERIC = "default_numeric";
119     private static final String DEFAULT_QUANTIFIABLE = "default_quantifiable";
120
121     private static final Set<String> SUPPORTED_TYPES = Set.of(CoreItemFactory.SWITCH, CoreItemFactory.CONTACT,
122             CoreItemFactory.DIMMER, CoreItemFactory.NUMBER, CoreItemFactory.ROLLERSHUTTER, CoreItemFactory.COLOR);
123
124     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
125             new NamedThreadFactory("RRD4j"));
126
127     private final Map<String, RrdDefConfig> rrdDefs = new ConcurrentHashMap<>();
128
129     private final ConcurrentSkipListMap<Key, Double> storageMap = new ConcurrentSkipListMap<>(Key::compareTo);
130
131     private static final String DATASOURCE_STATE = "state";
132
133     private static final Path DB_FOLDER = Path.of(OpenHAB.getUserDataFolder(), "persistence", "rrd4j").toAbsolutePath();
134
135     private static final RrdDbPool DATABASE_POOL = new RrdDbPool();
136
137     private final Logger logger = LoggerFactory.getLogger(RRD4jPersistenceService.class);
138     private final ItemRegistry itemRegistry;
139     private boolean active = false;
140
141     public static Path getDatabasePath(String name) {
142         return DB_FOLDER.resolve(name + ".rrd");
143     }
144
145     public static RrdDbPool getDatabasePool() {
146         return DATABASE_POOL;
147     }
148
149     private final ScheduledFuture<?> storeJob;
150
151     @Activate
152     public RRD4jPersistenceService(final @Reference ItemRegistry itemRegistry, Map<String, Object> config) {
153         this.itemRegistry = itemRegistry;
154         storeJob = scheduler.scheduleWithFixedDelay(() -> doStore(false), 1, 1, TimeUnit.SECONDS);
155         modified(config);
156         active = true;
157     }
158
159     @Modified
160     protected void modified(final Map<String, Object> config) {
161         // clean existing definitions
162         rrdDefs.clear();
163
164         // add default configurations
165
166         RrdDefConfig defaultNumeric = new RrdDefConfig(DEFAULT_NUMERIC);
167         // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
168         defaultNumeric.setDef("GAUGE,600,U,U,10");
169         // define 5 different boxes:
170         // 1. granularity of 10s for the last hour
171         // 2. granularity of 1m for the last week
172         // 3. granularity of 15m for the last year
173         // 4. granularity of 1h for the last 5 years
174         // 5. granularity of 1d for the last 10 years
175         defaultNumeric
176                 .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");
177         rrdDefs.put(DEFAULT_NUMERIC, defaultNumeric);
178
179         RrdDefConfig defaultQuantifiable = new RrdDefConfig(DEFAULT_QUANTIFIABLE);
180         // use 10 seconds as a step size for numeric values and allow a 10 minute silence between updates
181         defaultQuantifiable.setDef("GAUGE,600,U,U,10");
182         // define 5 different boxes:
183         // 1. granularity of 10s for the last hour
184         // 2. granularity of 1m for the last week
185         // 3. granularity of 15m for the last year
186         // 4. granularity of 1h for the last 5 years
187         // 5. granularity of 1d for the last 10 years
188         defaultQuantifiable.addArchives(
189                 "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");
190         rrdDefs.put(DEFAULT_QUANTIFIABLE, defaultQuantifiable);
191
192         RrdDefConfig defaultOther = new RrdDefConfig(DEFAULT_OTHER);
193         // use 5 seconds as a step size for discrete values and allow a 1h silence between updates
194         defaultOther.setDef("GAUGE,3600,U,U,5");
195         // define 4 different boxes:
196         // 1. granularity of 5s for the last hour
197         // 2. granularity of 1m for the last week
198         // 3. granularity of 15m for the last year
199         // 4. granularity of 4h for the last 10 years
200         defaultOther.addArchives("LAST,0.5,1,720:LAST,0.5,12,10080:LAST,0.5,180,35040:LAST,0.5,2880,21900");
201         rrdDefs.put(DEFAULT_OTHER, defaultOther);
202
203         if (config.isEmpty()) {
204             logger.debug("using default configuration only");
205             return;
206         }
207
208         Iterator<String> keys = config.keySet().iterator();
209         while (keys.hasNext()) {
210             String key = keys.next();
211
212             if ("service.pid".equals(key) || "component.name".equals(key)) {
213                 // ignore service.pid and name
214                 continue;
215             }
216
217             String[] subkeys = key.split("\\.");
218             if (subkeys.length != 2) {
219                 logger.debug("config '{}' should have the format 'name.configkey'", key);
220                 continue;
221             }
222
223             Object v = config.get(key);
224             if (v instanceof String value) {
225                 String name = subkeys[0].toLowerCase();
226                 String property = subkeys[1].toLowerCase();
227
228                 if (value.isBlank()) {
229                     logger.trace("Config is empty: {}", property);
230                     continue;
231                 } else {
232                     logger.trace("Processing config: {} = {}", property, value);
233                 }
234
235                 RrdDefConfig rrdDef = rrdDefs.get(name);
236                 if (rrdDef == null) {
237                     rrdDef = new RrdDefConfig(name);
238                     rrdDefs.put(name, rrdDef);
239                 }
240
241                 try {
242                     if ("def".equals(property)) {
243                         rrdDef.setDef(value);
244                     } else if ("archives".equals(property)) {
245                         rrdDef.addArchives(value);
246                     } else if ("items".equals(property)) {
247                         rrdDef.addItems(value);
248                     } else {
249                         logger.debug("Unknown property {} : {}", property, value);
250                     }
251                 } catch (IllegalArgumentException e) {
252                     logger.warn("Ignoring illegal configuration: {}", e.getMessage());
253                 }
254             }
255         }
256
257         for (RrdDefConfig rrdDef : rrdDefs.values()) {
258             if (rrdDef.isValid()) {
259                 logger.debug("Created {}", rrdDef);
260             } else {
261                 logger.info("Removing invalid definition {}", rrdDef);
262                 rrdDefs.remove(rrdDef.name);
263             }
264         }
265     }
266
267     @Deactivate
268     protected void deactivate() {
269         active = false;
270         storeJob.cancel(false);
271
272         // make sure we really store everything
273         doStore(true);
274     }
275
276     @Override
277     public String getId() {
278         return SERVICE_ID;
279     }
280
281     @Override
282     public String getLabel(@Nullable Locale locale) {
283         return "RRD4j";
284     }
285
286     @Override
287     public void store(final Item item, @Nullable final String alias) {
288         if (!active) {
289             logger.warn("Tried to store {} but service is not yet ready (or shutting down).", item);
290             return;
291         }
292
293         if (!isSupportedItemType(item)) {
294             logger.trace("Ignoring item '{}' since its type {} is not supported", item.getName(), item.getType());
295             return;
296         }
297         final String name = alias == null ? item.getName() : alias;
298
299         Double value;
300
301         if (item instanceof NumberItem nItem && item.getState() instanceof QuantityType<?> qState) {
302             Unit<? extends Quantity<?>> unit = nItem.getUnit();
303             if (unit != null) {
304                 QuantityType<?> convertedState = qState.toUnit(unit);
305                 if (convertedState != null) {
306                     value = convertedState.doubleValue();
307                 } else {
308                     value = null;
309                     logger.warn(
310                             "Failed to convert state '{}' to unit '{}'. Please check your item definition for correctness.",
311                             qState, unit);
312                 }
313             } else {
314                 value = qState.doubleValue();
315             }
316         } else {
317             DecimalType state = item.getStateAs(DecimalType.class);
318             if (state != null) {
319                 value = state.toBigDecimal().doubleValue();
320             } else {
321                 value = null;
322             }
323         }
324
325         if (value == null) {
326             // we could not convert the value
327             return;
328         }
329
330         long now = System.currentTimeMillis() / 1000;
331         Double oldValue = storageMap.put(new Key(now, name), value);
332         if (oldValue != null && !oldValue.equals(value)) {
333             logger.debug(
334                     "Discarding value {} for item {} with timestamp {} because a new value ({}) arrived with the same timestamp.",
335                     oldValue, name, now, value);
336         }
337     }
338
339     private void doStore(boolean force) {
340         long now = System.currentTimeMillis() / 1000;
341         while (!storageMap.isEmpty()) {
342             Key key = storageMap.firstKey();
343             if (now > key.timestamp || force) {
344                 // no new elements can be added for this timestamp because we are already past that time or the service
345                 // requires forced storing
346                 Double value = storageMap.pollFirstEntry().getValue();
347                 writePointToDatabase(key.name, value, key.timestamp);
348             } else {
349                 return;
350             }
351         }
352     }
353
354     private synchronized void writePointToDatabase(String name, double value, long timestamp) {
355         RrdDb db = null;
356         try {
357             db = getDB(name, true);
358         } catch (Exception e) {
359             logger.warn("Failed to open rrd4j database '{}' to store data ({})", name, e.toString());
360         }
361         if (db == null) {
362             return;
363         }
364
365         ConsolFun function = getConsolidationFunction(db);
366         if (function != ConsolFun.AVERAGE) {
367             try {
368                 // we store the last value again, so that the value change
369                 // in the database is not interpolated, but
370                 // happens right at this spot
371                 if (timestamp - 1 > db.getLastUpdateTime()) {
372                     // only do it if there is not already a value
373                     double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
374                     if (!Double.isNaN(lastValue)) {
375                         Sample sample = db.createSample();
376                         sample.setTime(timestamp - 1);
377                         sample.setValue(DATASOURCE_STATE, lastValue);
378                         sample.update();
379                         logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database (again)", name,
380                                 lastValue, timestamp - 1);
381                     }
382                 }
383             } catch (IOException e) {
384                 logger.debug("Error storing last value (again) for {}: {}", e.getMessage(), name);
385             }
386         }
387         try {
388             Sample sample = db.createSample();
389             sample.setTime(timestamp);
390             double storeValue = value;
391             if (db.getDatasource(DATASOURCE_STATE).getType() == DsType.COUNTER) {
392                 // counter values must be adjusted by stepsize
393                 storeValue = value * db.getRrdDef().getStep();
394             }
395             sample.setValue(DATASOURCE_STATE, storeValue);
396             sample.update();
397             logger.debug("Stored '{}' as value '{}' with timestamp {} in rrd4j database", name, storeValue, timestamp);
398         } catch (Exception e) {
399             logger.warn("Could not persist '{}' to rrd4j database: {}", name, e.getMessage());
400         }
401         try {
402             db.close();
403         } catch (IOException e) {
404             logger.debug("Error closing rrd4j database: {}", e.getMessage());
405         }
406     }
407
408     @Override
409     public void store(Item item) {
410         store(item, null);
411     }
412
413     @Override
414     public Iterable<HistoricItem> query(FilterCriteria filter) {
415         ZonedDateTime filterBeginDate = filter.getBeginDate();
416         ZonedDateTime filterEndDate = filter.getEndDate();
417         if (filterBeginDate != null && filterEndDate != null && filterBeginDate.isAfter(filterEndDate)) {
418             throw new IllegalArgumentException("begin (" + filterBeginDate + ") before end (" + filterEndDate + ")");
419         }
420
421         String itemName = filter.getItemName();
422         if (itemName == null) {
423             logger.warn("Item name is missing in filter {}", filter);
424             return List.of();
425         }
426         logger.trace("Querying rrd4j database for item '{}'", itemName);
427
428         RrdDb db = null;
429         try {
430             db = getDB(itemName, false);
431         } catch (Exception e) {
432             logger.warn("Failed to open rrd4j database '{}' for querying ({})", itemName, e.toString());
433             return List.of();
434         }
435         if (db == null) {
436             logger.debug("Could not find item '{}' in rrd4j database", itemName);
437             return List.of();
438         }
439
440         Item item = null;
441         Unit<?> unit = null;
442         try {
443             item = itemRegistry.getItem(itemName);
444             if (item instanceof NumberItem numberItem) {
445                 // we already retrieve the unit here once as it is a very costly operation,
446                 // see https://github.com/openhab/openhab-addons/issues/8928
447                 unit = numberItem.getUnit();
448             }
449         } catch (ItemNotFoundException e) {
450             logger.debug("Could not find item '{}' in registry", itemName);
451         }
452
453         long start = 0L;
454         long end = filterEndDate == null ? System.currentTimeMillis() / 1000
455                 : filterEndDate.toInstant().getEpochSecond();
456
457         DoubleFunction<State> toState = toStateMapper(item, unit);
458
459         try {
460             if (filterBeginDate == null) {
461                 // as rrd goes back for years and gets more and more inaccurate, we only support descending order
462                 // and a single return value if no begin date is given - this case is required specifically for the
463                 // historicState() query, which we want to support
464                 if (filter.getOrdering() == Ordering.DESCENDING && filter.getPageSize() == 1
465                         && filter.getPageNumber() == 0) {
466                     if (filterEndDate == null || Duration.between(filterEndDate, ZonedDateTime.now()).getSeconds() < db
467                             .getRrdDef().getStep()) {
468                         // we are asked only for the most recent value!
469                         double lastValue = db.getLastDatasourceValue(DATASOURCE_STATE);
470                         if (!Double.isNaN(lastValue)) {
471                             HistoricItem rrd4jItem = new RRD4jItem(itemName, toState.apply(lastValue),
472                                     ZonedDateTime.ofInstant(Instant.ofEpochSecond(db.getLastArchiveUpdateTime()),
473                                             ZoneId.systemDefault()));
474                             return List.of(rrd4jItem);
475                         } else {
476                             return List.of();
477                         }
478                     } else {
479                         start = end;
480                     }
481                 } else {
482                     throw new UnsupportedOperationException(
483                             "rrd4j does not allow querys without a begin date, unless order is descending and a single value is requested");
484                 }
485             } else {
486                 start = filterBeginDate.toInstant().getEpochSecond();
487             }
488
489             // do not call method {@link RrdDb#createFetchRequest(ConsolFun, long, long, long)} if start > end to avoid
490             // an IAE to be thrown
491             if (start > end) {
492                 logger.debug("Could not query rrd4j database for item '{}': start ({}) > end ({})", itemName, start,
493                         end);
494                 return List.of();
495             }
496
497             FetchRequest request = db.createFetchRequest(getConsolidationFunction(db), start, end, 1);
498             FetchData result = request.fetchData();
499
500             List<HistoricItem> items = new ArrayList<>();
501             long ts = result.getFirstTimestamp();
502             ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(ts), ZoneId.systemDefault());
503             long step = result.getRowCount() > 1 ? result.getStep() : 0;
504
505             double prevValue = Double.NaN;
506             State prevState = null;
507             for (double value : result.getValues(DATASOURCE_STATE)) {
508                 if (!Double.isNaN(value) && (((ts >= start) && (ts <= end)) || (start == end))) {
509                     State state;
510
511                     if (prevValue == value) {
512                         state = prevState;
513                     } else {
514                         prevState = state = toState.apply(value);
515                         prevValue = value;
516                     }
517
518                     RRD4jItem rrd4jItem = new RRD4jItem(itemName, state, zdt);
519                     items.add(rrd4jItem);
520                 }
521                 zdt = zdt.plusSeconds(step);
522                 ts += step;
523             }
524             return items;
525         } catch (IOException e) {
526             logger.warn("Could not query rrd4j database for item '{}': {}", itemName, e.getMessage());
527             return List.of();
528         } finally {
529             try {
530                 db.close();
531             } catch (IOException e) {
532                 logger.debug("Error closing rrd4j database: {}", e.getMessage());
533             }
534         }
535     }
536
537     @Override
538     public Set<PersistenceItemInfo> getItemInfo() {
539         return Set.of();
540     }
541
542     protected synchronized @Nullable RrdDb getDB(String alias, boolean createFileIfAbsent) {
543         RrdDb db = null;
544         Path path = getDatabasePath(alias);
545         try {
546             Builder builder = RrdDb.getBuilder();
547             builder.setPool(DATABASE_POOL);
548
549             if (Files.exists(path)) {
550                 // recreate the RrdDb instance from the file
551                 builder.setPath(path.toString());
552                 db = builder.build();
553             } else if (createFileIfAbsent) {
554                 if (!Files.exists(DB_FOLDER)) {
555                     Files.createDirectories(DB_FOLDER);
556                 }
557                 RrdDef rrdDef = getRrdDef(alias, path);
558                 if (rrdDef != null) {
559                     // create a new database file
560                     builder.setRrdDef(rrdDef);
561                     db = builder.build();
562                 } else {
563                     logger.debug(
564                             "Did not create rrd4j database for item '{}' since no rrd definition could be determined. This is likely due to an unsupported item type.",
565                             alias);
566                 }
567             }
568         } catch (IOException e) {
569             logger.error("Could not create rrd4j database file '{}': {}", path, e.getMessage());
570         } catch (RejectedExecutionException e) {
571             // this happens if the system is shut down
572             logger.debug("Could not create rrd4j database file '{}': {}", path, e.getMessage());
573         }
574         return db;
575     }
576
577     private @Nullable RrdDefConfig getRrdDefConfig(String itemName) {
578         RrdDefConfig useRdc = null;
579         for (Map.Entry<String, RrdDefConfig> e : rrdDefs.entrySet()) {
580             // try to find special config
581             RrdDefConfig rdc = e.getValue();
582             if (rdc.appliesTo(itemName)) {
583                 useRdc = rdc;
584                 break;
585             }
586         }
587         if (useRdc == null) { // not defined, use defaults
588             try {
589                 Item item = itemRegistry.getItem(itemName);
590                 if (!isSupportedItemType(item)) {
591                     return null;
592                 }
593                 if (item instanceof NumberItem numberItem) {
594                     useRdc = numberItem.getDimension() != null ? rrdDefs.get(DEFAULT_QUANTIFIABLE)
595                             : rrdDefs.get(DEFAULT_NUMERIC);
596                 } else {
597                     useRdc = rrdDefs.get(DEFAULT_OTHER);
598                 }
599             } catch (ItemNotFoundException e) {
600                 logger.debug("Could not find item '{}' in registry", itemName);
601                 return null;
602             }
603         }
604         logger.trace("Using rrd definition '{}' for item '{}'.", useRdc, itemName);
605         return useRdc;
606     }
607
608     private @Nullable RrdDef getRrdDef(String itemName, Path path) {
609         RrdDef rrdDef = new RrdDef(path.toString());
610         RrdDefConfig useRdc = getRrdDefConfig(itemName);
611         if (useRdc != null) {
612             rrdDef.setStep(useRdc.step);
613             rrdDef.setStartTime(System.currentTimeMillis() / 1000 - useRdc.step);
614             rrdDef.addDatasource(DATASOURCE_STATE, useRdc.dsType, useRdc.heartbeat, useRdc.min, useRdc.max);
615             for (RrdArchiveDef rad : useRdc.archives) {
616                 rrdDef.addArchive(rad.fcn, rad.xff, rad.steps, rad.rows);
617             }
618             return rrdDef;
619         } else {
620             return null;
621         }
622     }
623
624     public ConsolFun getConsolidationFunction(RrdDb db) {
625         try {
626             return db.getRrdDef().getArcDefs()[0].getConsolFun();
627         } catch (IOException e) {
628             return ConsolFun.MAX;
629         }
630     }
631
632     private <Q extends Quantity<Q>> DoubleFunction<State> toStateMapper(@Nullable Item item, @Nullable Unit<Q> unit) {
633         if (item instanceof GroupItem groupItem) {
634             item = groupItem.getBaseItem();
635         }
636
637         if (item instanceof SwitchItem && !(item instanceof DimmerItem)) {
638             return (value) -> OnOffType.from(value != 0.0d);
639         } else if (item instanceof ContactItem) {
640             return (value) -> value == 0.0d ? OpenClosedType.CLOSED : OpenClosedType.OPEN;
641         } else if (item instanceof DimmerItem || item instanceof RollershutterItem || item instanceof ColorItem) {
642             // make sure Items that need PercentTypes instead of DecimalTypes do receive the right information
643             return (value) -> new PercentType((int) Math.round(value * 100));
644         } else if (item instanceof NumberItem) {
645             if (unit != null) {
646                 return (value) -> new QuantityType<>(value, unit);
647             }
648         }
649         return DecimalType::new;
650     }
651
652     private boolean isSupportedItemType(Item item) {
653         if (item instanceof GroupItem groupItem) {
654             final Item baseItem = groupItem.getBaseItem();
655             if (baseItem != null) {
656                 item = baseItem;
657             }
658         }
659
660         return SUPPORTED_TYPES.contains(ItemUtil.getMainItemType(item.getType()));
661     }
662
663     public List<String> getRrdFiles() {
664         try (Stream<Path> stream = Files.list(DB_FOLDER)) {
665             return stream.filter(file -> !Files.isDirectory(file) && file.toFile().getName().endsWith(".rrd"))
666                     .map(file -> file.toFile().getName()).collect(Collectors.toList());
667         } catch (IOException e) {
668             return List.of();
669         }
670     }
671
672     private static class RrdArchiveDef {
673         public @Nullable ConsolFun fcn;
674         public double xff;
675         public int steps, rows;
676
677         @Override
678         public String toString() {
679             StringBuilder sb = new StringBuilder(" " + fcn);
680             sb.append(" xff = ").append(xff);
681             sb.append(" steps = ").append(steps);
682             sb.append(" rows = ").append(rows);
683             return sb.toString();
684         }
685     }
686
687     private class RrdDefConfig {
688         public String name;
689         public @Nullable DsType dsType;
690         public int heartbeat, step;
691         public double min, max;
692         public List<RrdArchiveDef> archives;
693         public List<String> itemNames;
694
695         private boolean isInitialized;
696
697         public RrdDefConfig(String name) {
698             this.name = name;
699             archives = new ArrayList<>();
700             itemNames = new ArrayList<>();
701             isInitialized = false;
702         }
703
704         public void setDef(String defString) {
705             String[] opts = defString.split(",");
706             if (opts.length != 5) { // check if correct number of parameters
707                 logger.warn("invalid number of parameters {}: {}", name, defString);
708                 return;
709             }
710
711             if ("ABSOLUTE".equals(opts[0])) { // dsType
712                 dsType = DsType.ABSOLUTE;
713             } else if ("COUNTER".equals(opts[0])) {
714                 dsType = DsType.COUNTER;
715             } else if ("DERIVE".equals(opts[0])) {
716                 dsType = DsType.DERIVE;
717             } else if ("GAUGE".equals(opts[0])) {
718                 dsType = DsType.GAUGE;
719             } else {
720                 logger.warn("{}: dsType {} not supported", name, opts[0]);
721             }
722
723             heartbeat = Integer.parseInt(opts[1]);
724
725             if ("U".equals(opts[2])) {
726                 min = Double.NaN;
727             } else {
728                 min = Double.parseDouble(opts[2]);
729             }
730
731             if ("U".equals(opts[3])) {
732                 max = Double.NaN;
733             } else {
734                 max = Double.parseDouble(opts[3]);
735             }
736
737             step = Integer.parseInt(opts[4]);
738
739             isInitialized = true; // successfully initialized
740
741             return;
742         }
743
744         public void addArchives(String archivesString) {
745             String[] splitArchives = archivesString.split(":");
746             for (String archiveString : splitArchives) {
747                 String[] opts = archiveString.split(",");
748                 if (opts.length != 4) { // check if correct number of parameters
749                     logger.warn("invalid number of parameters {}: {}", name, archiveString);
750                     return;
751                 }
752                 RrdArchiveDef arc = new RrdArchiveDef();
753
754                 if ("AVERAGE".equals(opts[0])) {
755                     arc.fcn = ConsolFun.AVERAGE;
756                 } else if ("MIN".equals(opts[0])) {
757                     arc.fcn = ConsolFun.MIN;
758                 } else if ("MAX".equals(opts[0])) {
759                     arc.fcn = ConsolFun.MAX;
760                 } else if ("LAST".equals(opts[0])) {
761                     arc.fcn = ConsolFun.LAST;
762                 } else if ("FIRST".equals(opts[0])) {
763                     arc.fcn = ConsolFun.FIRST;
764                 } else if ("TOTAL".equals(opts[0])) {
765                     arc.fcn = ConsolFun.TOTAL;
766                 } else {
767                     logger.warn("{}: consolidation function  {} not supported", name, opts[0]);
768                 }
769                 arc.xff = Double.parseDouble(opts[1]);
770                 arc.steps = Integer.parseInt(opts[2]);
771                 arc.rows = Integer.parseInt(opts[3]);
772                 archives.add(arc);
773             }
774         }
775
776         public void addItems(String itemsString) {
777             Collections.addAll(itemNames, itemsString.split(","));
778         }
779
780         public boolean appliesTo(String item) {
781             return itemNames.contains(item);
782         }
783
784         public boolean isValid() { // a valid configuration must be initialized
785             // and contain at least one function
786             return isInitialized && !archives.isEmpty();
787         }
788
789         @Override
790         public String toString() {
791             StringBuilder sb = new StringBuilder(name);
792             sb.append(" = ").append(dsType);
793             sb.append(" heartbeat = ").append(heartbeat);
794             sb.append(" min/max = ").append(min).append("/").append(max);
795             sb.append(" step = ").append(step);
796             sb.append(" ").append(archives.size()).append(" archives(s) = [");
797             for (RrdArchiveDef arc : archives) {
798                 sb.append(arc.toString());
799             }
800             sb.append("] ");
801             sb.append(itemNames.size()).append(" items(s) = [");
802             for (String item : itemNames) {
803                 sb.append(item).append(" ");
804             }
805             sb.append("]");
806             return sb.toString();
807         }
808     }
809
810     @Override
811     public List<PersistenceStrategy> getDefaultStrategies() {
812         return List.of(PersistenceStrategy.Globals.RESTORE, PersistenceStrategy.Globals.CHANGE,
813                 new PersistenceCronStrategy("everyMinute", "0 * * * * ?"));
814     }
815 }