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