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