]> git.basschouten.com Git - openhab-addons.git/blob
f683b5e1ecb1dbb61ef87625bba585d8568e348f
[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.mongodb.internal;
14
15 import java.time.ZoneId;
16 import java.time.ZonedDateTime;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.Date;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.Map;
23 import java.util.Set;
24
25 import org.bson.types.ObjectId;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.core.items.Item;
29 import org.openhab.core.items.ItemNotFoundException;
30 import org.openhab.core.items.ItemRegistry;
31 import org.openhab.core.library.items.ContactItem;
32 import org.openhab.core.library.items.DateTimeItem;
33 import org.openhab.core.library.items.DimmerItem;
34 import org.openhab.core.library.items.NumberItem;
35 import org.openhab.core.library.items.RollershutterItem;
36 import org.openhab.core.library.items.SwitchItem;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.OpenClosedType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.persistence.FilterCriteria;
44 import org.openhab.core.persistence.FilterCriteria.Operator;
45 import org.openhab.core.persistence.FilterCriteria.Ordering;
46 import org.openhab.core.persistence.HistoricItem;
47 import org.openhab.core.persistence.PersistenceItemInfo;
48 import org.openhab.core.persistence.PersistenceService;
49 import org.openhab.core.persistence.QueryablePersistenceService;
50 import org.openhab.core.persistence.strategy.PersistenceStrategy;
51 import org.openhab.core.types.State;
52 import org.openhab.core.types.UnDefType;
53 import org.osgi.framework.BundleContext;
54 import org.osgi.service.component.annotations.Activate;
55 import org.osgi.service.component.annotations.Component;
56 import org.osgi.service.component.annotations.ConfigurationPolicy;
57 import org.osgi.service.component.annotations.Deactivate;
58 import org.osgi.service.component.annotations.Reference;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import com.mongodb.BasicDBObject;
63 import com.mongodb.DBCollection;
64 import com.mongodb.DBCursor;
65 import com.mongodb.DBObject;
66 import com.mongodb.MongoClient;
67 import com.mongodb.MongoClientURI;
68
69 /**
70  * This is the implementation of the MongoDB {@link PersistenceService}.
71  *
72  * @author Thorsten Hoeger - Initial contribution
73  * @author Stephan Brunner - Query fixes, Cleanup
74  */
75 @NonNullByDefault
76 @Component(service = { PersistenceService.class,
77         QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
78 public class MongoDBPersistenceService implements QueryablePersistenceService {
79
80     private static final String FIELD_ID = "_id";
81     private static final String FIELD_ITEM = "item";
82     private static final String FIELD_REALNAME = "realName";
83     private static final String FIELD_TIMESTAMP = "timestamp";
84     private static final String FIELD_VALUE = "value";
85
86     private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
87
88     private String url = "";
89     private String db = "";
90     private String collection = "";
91     private boolean collectionPerItem;
92
93     private boolean initialized = false;
94
95     protected final ItemRegistry itemRegistry;
96
97     private @Nullable MongoClient cl;
98
99     @Activate
100     public MongoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
101         this.itemRegistry = itemRegistry;
102     }
103
104     @Activate
105     public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
106         @Nullable
107         String configUrl = (String) config.get("url");
108         logger.debug("MongoDB URL {}", configUrl);
109         if (configUrl == null || configUrl.isBlank()) {
110             logger.warn("The MongoDB database URL is missing - please configure the mongodb:url parameter.");
111             return;
112         }
113         url = configUrl;
114
115         @Nullable
116         String configDb = (String) config.get("database");
117         logger.debug("MongoDB database {}", configDb);
118         if (configDb == null || configDb.isBlank()) {
119             logger.warn("The MongoDB database name is missing - please configure the mongodb:database parameter.");
120             return;
121         }
122         db = configDb;
123
124         @Nullable
125         String dbCollection = (String) config.get("collection");
126         logger.debug("MongoDB collection {}", dbCollection);
127         collection = dbCollection == null ? "" : dbCollection;
128         collectionPerItem = dbCollection == null || dbCollection.isBlank();
129
130         if (!tryConnectToDatabase()) {
131             logger.warn("Failed to connect to MongoDB server. Trying to reconnect later.");
132         }
133
134         initialized = true;
135     }
136
137     @Deactivate
138     public void deactivate(final int reason) {
139         logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
140         disconnectFromDatabase();
141     }
142
143     @Override
144     public String getId() {
145         return "mongodb";
146     }
147
148     @Override
149     public String getLabel(@Nullable Locale locale) {
150         return "MongoDB";
151     }
152
153     @Override
154     public void store(Item item, @Nullable String alias) {
155         // Don't log undefined/uninitialized data
156         if (item.getState() instanceof UnDefType) {
157             return;
158         }
159
160         // If we've not initialized the bundle, then return
161         if (!initialized) {
162             logger.warn("MongoDB not initialized");
163             return;
164         }
165
166         // Connect to mongodb server if we're not already connected
167         // If we can't connect, log.
168         if (!tryConnectToDatabase()) {
169             logger.warn(
170                     "mongodb: No connection to database. Cannot persist item '{}'! Will retry connecting to database next time.",
171                     item);
172             return;
173         }
174
175         String realItemName = item.getName();
176         String collectionName = collectionPerItem ? realItemName : this.collection;
177
178         @Nullable
179         DBCollection collection = connectToCollection(collectionName);
180
181         if (collection == null) {
182             // Logging is done in connectToCollection()
183             return;
184         }
185
186         String name = (alias != null) ? alias : realItemName;
187         Object value = this.convertValue(item.getState());
188
189         DBObject obj = new BasicDBObject();
190         obj.put(FIELD_ID, new ObjectId());
191         obj.put(FIELD_ITEM, name);
192         obj.put(FIELD_REALNAME, realItemName);
193         obj.put(FIELD_TIMESTAMP, new Date());
194         obj.put(FIELD_VALUE, value);
195         collection.save(obj);
196
197         logger.debug("MongoDB save {}={}", name, value);
198     }
199
200     private Object convertValue(State state) {
201         Object value;
202         if (state instanceof PercentType type) {
203             value = type.toBigDecimal().doubleValue();
204         } else if (state instanceof DateTimeType type) {
205             value = Date.from(type.getZonedDateTime().toInstant());
206         } else if (state instanceof DecimalType type) {
207             value = type.toBigDecimal().doubleValue();
208         } else {
209             value = state.toString();
210         }
211         return value;
212     }
213
214     @Override
215     public void store(Item item) {
216         store(item, null);
217     }
218
219     @Override
220     public Set<PersistenceItemInfo> getItemInfo() {
221         return Collections.emptySet();
222     }
223
224     /**
225      * Checks if we have a database connection.
226      * Also tests if communication with the MongoDB-Server is available.
227      *
228      * @return true if connection has been established, false otherwise
229      */
230     private synchronized boolean isConnected() {
231         if (cl == null) {
232             return false;
233         }
234
235         // Also check if the connection is valid.
236         // Network problems may cause failure sometimes,
237         // even if the connection object was successfully created before.
238         try {
239             cl.getAddress();
240             return true;
241         } catch (Exception ex) {
242             return false;
243         }
244     }
245
246     /**
247      * (Re)connects to the database
248      *
249      * @return True, if the connection was successfully established.
250      */
251     private synchronized boolean tryConnectToDatabase() {
252         if (isConnected()) {
253             return true;
254         }
255
256         try {
257             logger.debug("Connect MongoDB");
258             disconnectFromDatabase();
259
260             this.cl = new MongoClient(new MongoClientURI(this.url));
261
262             // The mongo always succeeds in creating the connection.
263             // We have to actually force it to test the connection to try to connect to the server.
264             cl.getAddress();
265
266             logger.debug("Connect MongoDB ... done");
267             return true;
268         } catch (Exception e) {
269             logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
270             disconnectFromDatabase();
271             return false;
272         }
273     }
274
275     /**
276      * Fetches the currently valid database.
277      *
278      * @return The database object
279      */
280     private synchronized @Nullable MongoClient getDatabase() {
281         return cl;
282     }
283
284     /**
285      * Connects to the Collection
286      *
287      * @return The collection object when collection creation was successful. Null otherwise.
288      */
289     private @Nullable DBCollection connectToCollection(String collectionName) {
290         try {
291             @Nullable
292             MongoClient db = getDatabase();
293
294             if (db == null) {
295                 logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
296                 return null;
297             }
298
299             DBCollection mongoCollection = db.getDB(this.db).getCollection(collectionName);
300
301             BasicDBObject idx = new BasicDBObject();
302             idx.append(FIELD_ITEM, 1).append(FIELD_TIMESTAMP, 1);
303             mongoCollection.createIndex(idx);
304
305             return mongoCollection;
306         } catch (Exception e) {
307             logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
308             return null;
309         }
310     }
311
312     /**
313      * Disconnects from the database
314      */
315     private synchronized void disconnectFromDatabase() {
316         if (this.cl != null) {
317             this.cl.close();
318         }
319
320         cl = null;
321     }
322
323     @Override
324     public Iterable<HistoricItem> query(FilterCriteria filter) {
325         if (!initialized) {
326             return Collections.emptyList();
327         }
328
329         if (!tryConnectToDatabase()) {
330             return Collections.emptyList();
331         }
332
333         String realItemName = filter.getItemName();
334         if (realItemName == null) {
335             logger.warn("Item name is missing in filter {}", filter);
336             return List.of();
337         }
338
339         String collectionName = collectionPerItem ? realItemName : this.collection;
340         @Nullable
341         DBCollection collection = connectToCollection(collectionName);
342
343         // If collection creation failed, return nothing.
344         if (collection == null) {
345             // Logging is done in connectToCollection()
346             return Collections.emptyList();
347         }
348
349         @Nullable
350         Item item = getItem(realItemName);
351
352         if (item == null) {
353             logger.warn("Item {} not found", realItemName);
354             return Collections.emptyList();
355         }
356
357         List<HistoricItem> items = new ArrayList<>();
358         BasicDBObject query = new BasicDBObject();
359         if (filter.getItemName() != null) {
360             query.put(FIELD_ITEM, filter.getItemName());
361         }
362         State filterState = filter.getState();
363         if (filterState != null && filter.getOperator() != null) {
364             @Nullable
365             String op = convertOperator(filter.getOperator());
366
367             if (op == null) {
368                 logger.error("Failed to convert operator {} to MongoDB operator", filter.getOperator());
369                 return Collections.emptyList();
370             }
371
372             Object value = convertValue(filterState);
373             query.put(FIELD_VALUE, new BasicDBObject(op, value));
374         }
375
376         BasicDBObject dateQueries = new BasicDBObject();
377         if (filter.getBeginDate() != null) {
378             dateQueries.put("$gte", Date.from(filter.getBeginDate().toInstant()));
379         }
380         if (filter.getEndDate() != null) {
381             dateQueries.put("$lte", Date.from(filter.getEndDate().toInstant()));
382         }
383         if (!dateQueries.isEmpty()) {
384             query.put(FIELD_TIMESTAMP, dateQueries);
385         }
386
387         logger.debug("Query: {}", query);
388
389         Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
390         DBCursor cursor = collection.find(query).sort(new BasicDBObject(FIELD_TIMESTAMP, sortDir))
391                 .skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize());
392
393         while (cursor.hasNext()) {
394             BasicDBObject obj = (BasicDBObject) cursor.next();
395
396             final State state;
397             if (item instanceof NumberItem) {
398                 state = new DecimalType(obj.getDouble(FIELD_VALUE));
399             } else if (item instanceof DimmerItem) {
400                 state = new PercentType(obj.getInt(FIELD_VALUE));
401             } else if (item instanceof SwitchItem) {
402                 state = OnOffType.valueOf(obj.getString(FIELD_VALUE));
403             } else if (item instanceof ContactItem) {
404                 state = OpenClosedType.valueOf(obj.getString(FIELD_VALUE));
405             } else if (item instanceof RollershutterItem) {
406                 state = new PercentType(obj.getInt(FIELD_VALUE));
407             } else if (item instanceof DateTimeItem) {
408                 state = new DateTimeType(
409                         ZonedDateTime.ofInstant(obj.getDate(FIELD_VALUE).toInstant(), ZoneId.systemDefault()));
410             } else {
411                 state = new StringType(obj.getString(FIELD_VALUE));
412             }
413
414             items.add(new MongoDBItem(realItemName, state,
415                     ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
416         }
417
418         return items;
419     }
420
421     private @Nullable String convertOperator(Operator operator) {
422         switch (operator) {
423             case EQ:
424                 return "$eq";
425             case GT:
426                 return "$gt";
427             case GTE:
428                 return "$gte";
429             case LT:
430                 return "$lt";
431             case LTE:
432                 return "$lte";
433             case NEQ:
434                 return "$neq";
435             default:
436                 return null;
437         }
438     }
439
440     private @Nullable Item getItem(String itemName) {
441         try {
442             return itemRegistry.getItem(itemName);
443         } catch (ItemNotFoundException e1) {
444             logger.error("Unable to get item type for {}", itemName);
445         }
446         return null;
447     }
448
449     @Override
450     public List<PersistenceStrategy> getDefaultStrategies() {
451         return Collections.emptyList();
452     }
453 }