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