2 * Copyright (c) 2010-2021 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
75 @Component(service = { PersistenceService.class,
76 QueryablePersistenceService.class }, configurationPid = "org.openhab.mongodb", configurationPolicy = ConfigurationPolicy.REQUIRE)
77 public class MongoDBPersistenceService implements QueryablePersistenceService {
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";
85 private final Logger logger = LoggerFactory.getLogger(MongoDBPersistenceService.class);
87 private @NonNullByDefault({}) String url;
88 private @NonNullByDefault({}) String db;
89 private @NonNullByDefault({}) String collection;
90 private boolean collectionPerItem;
92 private boolean initialized = false;
94 protected final ItemRegistry itemRegistry;
96 private @NonNullByDefault({}) MongoClient cl;
97 private @NonNullByDefault({}) DBCollection mongoCollection;
100 public MongoDBPersistenceService(final @Reference ItemRegistry itemRegistry) {
101 this.itemRegistry = itemRegistry;
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.");
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.");
118 collection = (String) config.get("collection");
119 logger.debug("MongoDB collection {}", collection);
120 if (collection == null || collection.isBlank()) {
121 collectionPerItem = false;
123 collectionPerItem = true;
126 disconnectFromDatabase();
129 // connection has been established... initialization completed!
134 public void deactivate(final int reason) {
135 logger.debug("MongoDB persistence bundle stopping. Disconnecting from database.");
136 disconnectFromDatabase();
140 public String getId() {
145 public String getLabel(@Nullable Locale locale) {
150 public void store(Item item, @Nullable String alias) {
151 // Don't log undefined/uninitialized data
152 if (item.getState() instanceof UnDefType) {
156 // If we've not initialized the bundle, then return
158 logger.warn("MongoDB not initialized");
162 // Connect to mongodb server if we're not already connected
163 if (!isConnected()) {
167 // If we still didn't manage to connect, then return!
168 if (!isConnected()) {
170 "mongodb: No connection to database. Cannot persist item '{}'! Will retry connecting to database next time.",
175 String realName = item.getName();
177 // If collection Per Item is active, connect to the item Collection
178 if (collectionPerItem) {
179 connectToCollection(realName);
182 String name = (alias != null) ? alias : realName;
183 Object value = this.convertValue(item.getState());
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);
193 // If collection Per Item is active, disconnect after save.
194 if (collectionPerItem) {
195 disconnectFromCollection();
198 logger.debug("MongoDB save {}={}", name, value);
201 private Object convertValue(State state) {
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();
210 value = state.toString();
219 public void store(Item item) {
224 public Set<PersistenceItemInfo> getItemInfo() {
225 return Collections.emptySet();
229 * Checks if we have a database connection
231 * @return true if connection has been established, false otherwise
233 private boolean isConnected() {
238 * Connects to the database
240 private void connectToDatabase() {
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);
247 BasicDBObject idx = new BasicDBObject();
248 idx.append(FIELD_TIMESTAMP, 1).append(FIELD_ITEM, 1);
249 this.mongoCollection.createIndex(idx);
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);
260 * Connects to the Collection
262 private void connectToCollection(String collectionName) {
264 mongoCollection = cl.getDB(this.db).getCollection(collectionName);
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);
276 * Disconnects from the Collection
278 private void disconnectFromCollection() {
279 this.mongoCollection = null;
283 * Disconnects from the database
285 private void disconnectFromDatabase() {
286 this.mongoCollection = null;
287 if (this.cl != null) {
294 public Iterable<HistoricItem> query(FilterCriteria filter) {
296 return Collections.emptyList();
299 if (!isConnected()) {
303 if (!isConnected()) {
304 return Collections.emptyList();
307 String name = filter.getItemName();
309 // If collection Per Item is active, connect to the item Collection
310 if (collectionPerItem) {
311 connectToCollection(name);
313 Item item = getItem(name);
315 List<HistoricItem> items = new ArrayList<>();
316 DBObject query = new BasicDBObject();
317 if (filter.getItemName() != null) {
318 query.put(FIELD_ITEM, filter.getItemName());
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));
325 if (filter.getBeginDate() != null) {
326 query.put(FIELD_TIMESTAMP, new BasicDBObject("$gte", filter.getBeginDate()));
328 if (filter.getBeginDate() != null) {
329 query.put(FIELD_TIMESTAMP, new BasicDBObject("$lte", filter.getBeginDate()));
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());
336 while (cursor.hasNext()) {
337 BasicDBObject obj = (BasicDBObject) cursor.next();
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()));
354 state = new StringType(obj.getString(FIELD_VALUE));
357 items.add(new MongoDBItem(name, state,
358 ZonedDateTime.ofInstant(obj.getDate(FIELD_TIMESTAMP).toInstant(), ZoneId.systemDefault())));
361 // If collection Per Item is active, disconnect after save.
362 if (collectionPerItem) {
363 disconnectFromCollection();
368 private @Nullable String convertOperator(Operator operator) {
387 private @Nullable Item getItem(String itemName) {
389 return itemRegistry.getItem(itemName);
390 } catch (ItemNotFoundException e1) {
391 logger.error("Unable to get item type for {}", itemName);
397 public List<PersistenceStrategy> getDefaultStrategies() {
398 return Collections.emptyList();