2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.mongodb.internal;
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;
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;
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;
70 * This is the implementation of the MongoDB {@link PersistenceService}.
72 * @author Thorsten Hoeger - Initial contribution
73 * @author Stephan Brunner - Query fixes, Cleanup
76 @Component(service = { PersistenceService.class,
77 QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
78 public class MongoDBPersistenceService implements QueryablePersistenceService {
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";
86 private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
88 private String url = "";
89 private String db = "";
90 private String collection = "";
91 private boolean collectionPerItem;
93 private boolean initialized = false;
95 protected final ItemRegistry itemRegistry;
97 private @Nullable MongoClient cl;
100 public MongoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
101 this.itemRegistry = itemRegistry;
105 public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
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.");
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.");
125 String dbCollection = (String) config.get("collection");
126 logger.debug("MongoDB collection {}", dbCollection);
127 collection = dbCollection == null ? "" : dbCollection;
128 collectionPerItem = dbCollection == null || dbCollection.isBlank();
130 if (!tryConnectToDatabase()) {
131 logger.warn("Failed to connect to MongoDB server. Trying to reconnect later.");
138 public void deactivate(final int reason) {
139 logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
140 disconnectFromDatabase();
144 public String getId() {
149 public String getLabel(@Nullable Locale locale) {
154 public void store(Item item, @Nullable String alias) {
155 // Don't log undefined/uninitialized data
156 if (item.getState() instanceof UnDefType) {
160 // If we've not initialized the bundle, then return
162 logger.warn("MongoDB not initialized");
166 // Connect to mongodb server if we're not already connected
167 // If we can't connect, log.
168 if (!tryConnectToDatabase()) {
170 "mongodb: No connection to database. Cannot persist item '{}'! Will retry connecting to database next time.",
175 String realItemName = item.getName();
176 String collectionName = collectionPerItem ? realItemName : this.collection;
179 DBCollection collection = connectToCollection(collectionName);
181 if (collection == null) {
182 // Logging is done in connectToCollection()
186 String name = (alias != null) ? alias : realItemName;
187 Object value = this.convertValue(item.getState());
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);
197 logger.debug("MongoDB save {}={}", name, value);
200 private Object convertValue(State state) {
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();
209 value = state.toString();
215 public void store(Item item) {
220 public Set<PersistenceItemInfo> getItemInfo() {
221 return Collections.emptySet();
225 * Checks if we have a database connection.
226 * Also tests if communication with the MongoDB-Server is available.
228 * @return true if connection has been established, false otherwise
230 private synchronized boolean isConnected() {
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.
241 } catch (Exception ex) {
247 * (Re)connects to the database
249 * @return True, if the connection was successfully established.
251 private synchronized boolean tryConnectToDatabase() {
257 logger.debug("Connect MongoDB");
258 disconnectFromDatabase();
260 this.cl = new MongoClient(new MongoClientURI(this.url));
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.
266 logger.debug("Connect MongoDB ... done");
268 } catch (Exception e) {
269 logger.error("Failed to connect to database {}: {}", this.url, e.getMessage(), e);
270 disconnectFromDatabase();
276 * Fetches the currently valid database.
278 * @return The database object
280 private synchronized @Nullable MongoClient getDatabase() {
285 * Connects to the Collection
287 * @return The collection object when collection creation was successful. Null otherwise.
289 private @Nullable DBCollection connectToCollection(String collectionName) {
292 MongoClient db = getDatabase();
295 logger.error("Failed to connect to collection {}: Connection not ready", collectionName);
299 DBCollection mongoCollection = db.getDB(this.db).getCollection(collectionName);
301 BasicDBObject idx = new BasicDBObject();
302 idx.append(FIELD_ITEM, 1).append(FIELD_TIMESTAMP, 1);
303 mongoCollection.createIndex(idx);
305 return mongoCollection;
306 } catch (Exception e) {
307 logger.error("Failed to connect to collection {}: {}", collectionName, e.getMessage(), e);
313 * Disconnects from the database
315 private synchronized void disconnectFromDatabase() {
316 if (this.cl != null) {
324 public Iterable<HistoricItem> query(FilterCriteria filter) {
326 return Collections.emptyList();
329 if (!tryConnectToDatabase()) {
330 return Collections.emptyList();
333 String realItemName = filter.getItemName();
334 if (realItemName == null) {
335 logger.warn("Item name is missing in filter {}", filter);
339 String collectionName = collectionPerItem ? realItemName : this.collection;
341 DBCollection collection = connectToCollection(collectionName);
343 // If collection creation failed, return nothing.
344 if (collection == null) {
345 // Logging is done in connectToCollection()
346 return Collections.emptyList();
350 Item item = getItem(realItemName);
353 logger.warn("Item {} not found", realItemName);
354 return Collections.emptyList();
357 List<HistoricItem> items = new ArrayList<>();
358 BasicDBObject query = new BasicDBObject();
359 if (filter.getItemName() != null) {
360 query.put(FIELD_ITEM, filter.getItemName());
362 State filterState = filter.getState();
363 if (filterState != null && filter.getOperator() != null) {
365 String op = convertOperator(filter.getOperator());
368 logger.error("Failed to convert operator {} to MongoDB operator", filter.getOperator());
369 return Collections.emptyList();
372 Object value = convertValue(filterState);
373 query.put(FIELD_VALUE, new BasicDBObject(op, value));
376 BasicDBObject dateQueries = new BasicDBObject();
377 if (filter.getBeginDate() != null) {
378 dateQueries.put("$gte", Date.from(filter.getBeginDate().toInstant()));
380 if (filter.getEndDate() != null) {
381 dateQueries.put("$lte", Date.from(filter.getEndDate().toInstant()));
383 if (!dateQueries.isEmpty()) {
384 query.put(FIELD_TIMESTAMP, dateQueries);
387 logger.debug("Query: {}", query);
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());
393 while (cursor.hasNext()) {
394 BasicDBObject obj = (BasicDBObject) cursor.next();
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()));
411 state = new StringType(obj.getString(FIELD_VALUE));
414 items.add(new MongoDBItem(realItemName, state,
415 ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
421 private @Nullable String convertOperator(Operator operator) {
440 private @Nullable Item getItem(String itemName) {
442 return itemRegistry.getItem(itemName);
443 } catch (ItemNotFoundException e1) {
444 logger.error("Unable to get item type for {}", itemName);
450 public List<PersistenceStrategy> getDefaultStrategies() {
451 return Collections.emptyList();