]> git.basschouten.com Git - openhab-addons.git/blob
9d23d45d88e65b9fde6cf44bf54dd3b8b654ed18
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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  */
74 @NonNullByDefault
75 @Component(service = { PersistenceService.class,
76         QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
77 public class MongoDBPersistenceService implements QueryablePersistenceService {
78
79     private static final String FIELD_ID = "_id";
80     private static final String FIELD_ITEM = "item";
81     private static final String FIELD_REALNAME = "realName";
82     private static final String FIELD_TIMESTAMP = "timestamp";
83     private static final String FIELD_VALUE = "value";
84
85     private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
86
87     private @NonNullByDefault({}) String url;
88     private @NonNullByDefault({}) String db;
89     private @NonNullByDefault({}) String collection;
90     private boolean collectionPerItem;
91
92     private boolean initialized = false;
93
94     protected final ItemRegistry itemRegistry;
95
96     private @NonNullByDefault({}) MongoClient cl;
97     private @NonNullByDefault({}) DBCollection mongoCollection;
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         url = (String) config.get("url");
107         logger.debug("MongoDB URL {}", url);
108         if (url == null || url.isBlank()) {
109             logger.warn("The MongoDB database URL is missing - please configure the mongodb:url parameter.");
110             return;
111         }
112         db = (String) config.get("database");
113         logger.debug("MongoDB database {}", db);
114         if (db == null || db.isBlank()) {
115             logger.warn("The MongoDB database name is missing - please configure the mongodb:database parameter.");
116             return;
117         }
118         collection = (String) config.get("collection");
119         logger.debug("MongoDB collection {}", collection);
120         if (collection == null || collection.isBlank()) {
121             collectionPerItem = false;
122         } else {
123             collectionPerItem = true;
124         }
125
126         disconnectFromDatabase();
127         connectToDatabase();
128
129         // connection has been established... initialization completed!
130         initialized = true;
131     }
132
133     @Deactivate
134     public void deactivate(final int reason) {
135         logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
136         disconnectFromDatabase();
137     }
138
139     @Override
140     public String getId() {
141         return "mongodb";
142     }
143
144     @Override
145     public String getLabel(@Nullable Locale locale) {
146         return "Mongo DB";
147     }
148
149     @Override
150     public void store(Item item, @Nullable String alias) {
151         // Don't log undefined/uninitialized data
152         if (item.getState() instanceof UnDefType) {
153             return;
154         }
155
156         // If we've not initialized the bundle, then return
157         if (!initialized) {
158             logger.warn("MongoDB not initialized");
159             return;
160         }
161
162         // Connect to mongodb server if we're not already connected
163         if (!isConnected()) {
164             connectToDatabase();
165         }
166
167         // If we still didn't manage to connect, then return!
168         if (!isConnected()) {
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 realName = item.getName();
176
177         // If collection Per Item is active, connect to the item Collection
178         if (collectionPerItem) {
179             connectToCollection(realName);
180         }
181
182         String name = (alias != null) ? alias : realName;
183         Object value = this.convertValue(item.getState());
184
185         DBObject obj = new BasicDBObject();
186         obj.put(FIELD_ID, new ObjectId());
187         obj.put(FIELD_ITEM, name);
188         obj.put(FIELD_REALNAME, realName);
189         obj.put(FIELD_TIMESTAMP, new Date());
190         obj.put(FIELD_VALUE, value);
191         this.mongoCollection.save(obj);
192
193         // If collection Per Item is active, disconnect after save.
194         if (collectionPerItem) {
195             disconnectFromCollection();
196         }
197
198         logger.debug("MongoDB save {}={}", name, value);
199     }
200
201     private Object convertValue(State state) {
202         Object value;
203         if (state instanceof PercentType) {
204             value = ((PercentType) state).toBigDecimal().doubleValue();
205         } else if (state instanceof DateTimeType) {
206             value = Date.from(((DateTimeType) state).getZonedDateTime().toInstant());
207         } else if (state instanceof DecimalType) {
208             value = ((DecimalType) state).toBigDecimal().doubleValue();
209         } else {
210             value = state.toString();
211         }
212         return value;
213     }
214
215     /**
216      * @{inheritDoc
217      */
218     @Override
219     public void store(Item item) {
220         store(item, null);
221     }
222
223     @Override
224     public Set<PersistenceItemInfo> getItemInfo() {
225         return Collections.emptySet();
226     }
227
228     /**
229      * Checks if we have a database connection
230      *
231      * @return true if connection has been established, false otherwise
232      */
233     private boolean isConnected() {
234         return cl != null;
235     }
236
237     /**
238      * Connects to the database
239      */
240     private void connectToDatabase() {
241         try {
242             logger.debug("Connect MongoDB");
243             this.cl = new MongoClient(new MongoClientURI(this.url));
244             if (collectionPerItem) {
245                 mongoCollection = cl.getDB(this.db).getCollection(this.collection);
246
247                 BasicDBObject idx = new BasicDBObject();
248                 idx.append(FIELD_TIMESTAMP, 1).append(FIELD_ITEM, 1);
249                 this.mongoCollection.createIndex(idx);
250             }
251
252             logger.debug("Connect MongoDB ... done");
253         } catch (Exception e) {
254             logger.error("Failed to connect to database {}", this.url);
255             throw new RuntimeException("Cannot connect to database", e);
256         }
257     }
258
259     /**
260      * Connects to the Collection
261      */
262     private void connectToCollection(String collectionName) {
263         try {
264             mongoCollection = cl.getDB(this.db).getCollection(collectionName);
265
266             BasicDBObject idx = new BasicDBObject();
267             idx.append(FIELD_TIMESTAMP, 1).append(FIELD_ITEM, 1);
268             this.mongoCollection.createIndex(idx);
269         } catch (Exception e) {
270             logger.error("Failed to connect to collection {}", collectionName);
271             throw new RuntimeException("Cannot connect to collection", e);
272         }
273     }
274
275     /**
276      * Disconnects from the Collection
277      */
278     private void disconnectFromCollection() {
279         this.mongoCollection = null;
280     }
281
282     /**
283      * Disconnects from the database
284      */
285     private void disconnectFromDatabase() {
286         this.mongoCollection = null;
287         if (this.cl != null) {
288             this.cl.close();
289         }
290         cl = null;
291     }
292
293     @Override
294     public Iterable<HistoricItem> query(FilterCriteria filter) {
295         if (!initialized) {
296             return Collections.emptyList();
297         }
298
299         if (!isConnected()) {
300             connectToDatabase();
301         }
302
303         if (!isConnected()) {
304             return Collections.emptyList();
305         }
306
307         String name = filter.getItemName();
308
309         // If collection Per Item is active, connect to the item Collection
310         if (collectionPerItem) {
311             connectToCollection(name);
312         }
313         Item item = getItem(name);
314
315         List<HistoricItem> items = new ArrayList<>();
316         DBObject query = new BasicDBObject();
317         if (filter.getItemName() != null) {
318             query.put(FIELD_ITEM, filter.getItemName());
319         }
320         if (filter.getState() != null && filter.getOperator() != null) {
321             String op = convertOperator(filter.getOperator());
322             Object value = convertValue(filter.getState());
323             query.put(FIELD_VALUE, new BasicDBObject(op, value));
324         }
325         if (filter.getBeginDate() != null) {
326             query.put(FIELD_TIMESTAMP, new BasicDBObject("$gte", filter.getBeginDate()));
327         }
328         if (filter.getBeginDate() != null) {
329             query.put(FIELD_TIMESTAMP, new BasicDBObject("$lte", filter.getBeginDate()));
330         }
331
332         Integer sortDir = (filter.getOrdering() == Ordering.ASCENDING) ? 1 : -1;
333         DBCursor cursor = this.mongoCollection.find(query).sort(new BasicDBObject(FIELD_TIMESTAMP, sortDir))
334                 .skip(filter.getPageNumber() * filter.getPageSize()).limit(filter.getPageSize());
335
336         while (cursor.hasNext()) {
337             BasicDBObject obj = (BasicDBObject) cursor.next();
338
339             final State state;
340             if (item instanceof NumberItem) {
341                 state = new DecimalType(obj.getDouble(FIELD_VALUE));
342             } else if (item instanceof DimmerItem) {
343                 state = new PercentType(obj.getInt(FIELD_VALUE));
344             } else if (item instanceof SwitchItem) {
345                 state = OnOffType.valueOf(obj.getString(FIELD_VALUE));
346             } else if (item instanceof ContactItem) {
347                 state = OpenClosedType.valueOf(obj.getString(FIELD_VALUE));
348             } else if (item instanceof RollershutterItem) {
349                 state = new PercentType(obj.getInt(FIELD_VALUE));
350             } else if (item instanceof DateTimeItem) {
351                 state = new DateTimeType(
352                         ZonedDateTime.ofInstant(obj.getDate(FIELD_VALUE).toInstant(), ZoneId.systemDefault()));
353             } else {
354                 state = new StringType(obj.getString(FIELD_VALUE));
355             }
356
357             items.add(new MongoDBItem(name, state,
358                     ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
359         }
360
361         // If collection Per Item is active, disconnect after save.
362         if (collectionPerItem) {
363             disconnectFromCollection();
364         }
365         return items;
366     }
367
368     private @Nullable String convertOperator(Operator operator) {
369         switch (operator) {
370             case EQ:
371                 return "$eq";
372             case GT:
373                 return "$gt";
374             case GTE:
375                 return "$gte";
376             case LT:
377                 return "$lt";
378             case LTE:
379                 return "$lte";
380             case NEQ:
381                 return "$neq";
382             default:
383                 return null;
384         }
385     }
386
387     private @Nullable Item getItem(String itemName) {
388         try {
389             return itemRegistry.getItem(itemName);
390         } catch (ItemNotFoundException e1) {
391             logger.error("Unable to get item type for {}", itemName);
392         }
393         return null;
394     }
395
396     @Override
397     public List<PersistenceStrategy> getDefaultStrategies() {
398         return Collections.emptyList();
399     }
400 }