/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.persistence.influxdb.internal;
import static org.apache.commons.lang.StringUtils.isBlank;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
import org.influxdb.dto.Point;
import org.influxdb.dto.Pong;
import org.influxdb.dto.Query;
import org.influxdb.dto.QueryResult.Result;
import org.influxdb.dto.QueryResult.Series;
import org.openhab.core.items.Item;
import org.openhab.core.items.ItemNotFoundException;
import org.openhab.core.items.ItemRegistry;
import org.openhab.core.library.items.ColorItem;
import org.openhab.core.library.items.ContactItem;
import org.openhab.core.library.items.DateTimeItem;
import org.openhab.core.library.items.DimmerItem;
import org.openhab.core.library.items.LocationItem;
import org.openhab.core.library.items.NumberItem;
import org.openhab.core.library.items.RollershutterItem;
import org.openhab.core.library.items.SwitchItem;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.HSBType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.persistence.FilterCriteria;
import org.openhab.core.persistence.FilterCriteria.Ordering;
import org.openhab.core.persistence.HistoricItem;
import org.openhab.core.persistence.PersistenceService;
import org.openhab.core.persistence.QueryablePersistenceService;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import retrofit.RetrofitError;
/**
* This is the implementation of the InfluxDB {@link PersistenceService}. It persists item values
* using the <a href="http://influxdb.org">InfluxDB</a> time series database. The states (
* {@link State}) of an {@link Item} are persisted in a time series with names equal to the name of
* the item. All values are stored using integers or doubles, {@link OnOffType} and
* {@link OpenClosedType} are stored using 0 or 1.
*
* The defaults for the database name, the database user and the database url are "openhab",
* "openhab" and "http://127.0.0.1:8086".
*
* @author Theo Weiss - Initial Contribution, rewrite of org.openhab.persistence.influxdb > 0.9
* support
* @since 1.8.0
*/
public class InfluxDBPersistenceService implements QueryablePersistenceService {
private static final String DEFAULT_URL = "http://127.0.0.1:8086";
private static final String DEFAULT_DB = "openhab";
private static final String DEFAULT_USER = "openhab";
private static final String DEFAULT_RETENTION_POLICY = "autogen";
private static final String DIGITAL_VALUE_OFF = "0";
private static final String DIGITAL_VALUE_ON = "1";
private static final String VALUE_COLUMN_NAME = "value";
private ItemRegistry itemRegistry;
private InfluxDB influxDB;
private static final Logger logger = LoggerFactory.getLogger(InfluxDBPersistenceService.class);
private static final String TIME_COLUMN_NAME = "time";
private static final TimeUnit timeUnit = TimeUnit.MILLISECONDS;
private String dbName;
private String url;
private String user;
private String password;
private String retentionPolicy;
private boolean isProperlyConfigured;
private boolean connected;
public void setItemRegistry(ItemRegistry itemRegistry) {
this.itemRegistry = itemRegistry;
}
public void unsetItemRegistry(ItemRegistry itemRegistry) {
this.itemRegistry = null;
}
public void activate(final BundleContext bundleContext, final Map<String, Object> config) {
logger.debug("influxdb persistence service activated");
disconnect();
password = (String) config.get("password");
if (isBlank(password)) {
isProperlyConfigured = false;
logger.error("influxdb:password",
"The password is missing. To specify a password configure the password parameter in openhab.cfg.");
return;
}
url = (String) config.get("url");
if (isBlank(url)) {
url = DEFAULT_URL;
logger.debug("using default url {}", DEFAULT_URL);
}
user = (String) config.get("user");
if (isBlank(user)) {
user = DEFAULT_USER;
logger.debug("using default user {}", DEFAULT_USER);
}
dbName = (String) config.get("db");
if (isBlank(dbName)) {
dbName = DEFAULT_DB;
logger.debug("using default db name {}", DEFAULT_DB);
}
retentionPolicy = (String) config.get("retentionPolicy");
if (isBlank(retentionPolicy)) {
retentionPolicy = DEFAULT_RETENTION_POLICY;
logger.debug("using default retentionPolicy {}", DEFAULT_RETENTION_POLICY);
}
isProperlyConfigured = true;
connect();
// check connection; errors will only be logged, hoping the connection will work at a later
// time.
if (!checkConnection()) {
logger.error("database connection does not work for now, will retry to use the database.");
}
}
public void deactivate() {
logger.debug("influxdb persistence service deactivated");
disconnect();
}
private void connect() {
if (influxDB == null) {
// reuse an existing InfluxDB object because concerning the database it has no state
// connection
influxDB = InfluxDBFactory.connect(url, user, password);
influxDB.enableBatch(200, 100, timeUnit);
}
connected = true;
}
private boolean checkConnection() {
boolean dbStatus = false;
if (!connected) {
logger.error("checkConnection: database is not connected");
dbStatus = false;
} else {
try {
Pong pong = influxDB.ping();
String version = pong.getVersion();
// may be check for version >= 0.9
if (version != null && !version.contains("unknown")) {
dbStatus = true;
logger.debug("database status is OK, version is {}", version);
} else {
logger.error("database ping error, version is: \"{}\" response time was \"{}\"", version,
pong.getResponseTime());
dbStatus = false;
}
} catch (RuntimeException e) {
dbStatus = false;
logger.error("database connection failed", e);
handleDatabaseException(e);
}
}
return dbStatus;
}
private void disconnect() {
influxDB = null;
connected = false;
}
private boolean isConnected() {
return connected;
}
@Override
public String getName() {
return "influxdb";
}
@Override
public void store(Item item) {
store(item, null);
}
/**
* {@inheritDoc}
*/
@Override
public void store(Item item, String alias) {
if (item.getState() instanceof UnDefType) {
return;
}
if (!isProperlyConfigured) {
logger.warn("Configuration for influxdb not yet loaded or broken.");
return;
}
if (!isConnected()) {
logger.warn("InfluxDB is not yet connected");
return;
}
String realName = item.getName();
String name = (alias != null) ? alias : realName;
State state = null;
if (item.getAcceptedCommandTypes().contains(HSBType.class)) {
state = item.getStateAs(HSBType.class);
logger.trace("Tried to get item as {}, state is {}", HSBType.class, state.toString());
} else if (item.getAcceptedDataTypes().contains(PercentType.class)) {
state = item.getStateAs(PercentType.class);
logger.trace("Tried to get item as {}, state is {}", PercentType.class, state.toString());
} else {
// All other items should return the best format by default
state = item.getState();
logger.trace("Tried to get item from item class {}, state is {}", item.getClass(), state.toString());
}
Object value = stateToObject(state);
logger.trace("storing {} in influxdb value {}, {}", name, value, item);
Point point = Point.measurement(name).field(VALUE_COLUMN_NAME, value).time(System.currentTimeMillis(), timeUnit)
.build();
try {
influxDB.write(dbName, retentionPolicy, point);
} catch (RuntimeException e) {
logger.error("storing failed with exception for item: {}", name);
handleDatabaseException(e);
}
}
private void handleDatabaseException(Exception e) {
if (e instanceof RetrofitError) {
// e.g. raised if influxdb is not running
logger.error("database connection error {}", e.getMessage());
} else if (e instanceof RuntimeException) {
// e.g. raised by authentication errors
logger.error("database error: {}", e.getMessage());
}
}
@Override
public Iterable<HistoricItem> query(FilterCriteria filter) {
logger.debug("got a query");
if (!isProperlyConfigured) {
logger.warn("Configuration for influxdb not yet loaded or broken.");
return Collections.emptyList();
}
if (!isConnected()) {
logger.warn("InfluxDB is not yet connected");
return Collections.emptyList();
}
List<HistoricItem> historicItems = new ArrayList<HistoricItem>();
StringBuffer query = new StringBuffer();
query.append("select ").append(VALUE_COLUMN_NAME).append(' ').append("from \"").append(retentionPolicy)
.append("\".");
if (filter.getItemName() != null) {
query.append('"').append(filter.getItemName()).append('"');
} else {
query.append("/.*/");
}
logger.trace(
"Filter: itemname: {}, ordering: {}, state: {}, operator: {}, getBeginDate: {}, getEndDate: {}, getPageSize: {}, getPageNumber: {}",
filter.getItemName(), filter.getOrdering().toString(), filter.getState(), filter.getOperator(),
filter.getBeginDate(), filter.getEndDate(), filter.getPageSize(), filter.getPageNumber());
if ((filter.getState() != null && filter.getOperator() != null) || filter.getBeginDate() != null
|| filter.getEndDate() != null) {
query.append(" where ");
boolean foundState = false;
boolean foundBeginDate = false;
if (filter.getState() != null && filter.getOperator() != null) {
String value = stateToString(filter.getState());
if (value != null) {
foundState = true;
query.append(VALUE_COLUMN_NAME);
query.append(" ");
query.append(filter.getOperator().toString());
query.append(" ");
query.append(value);
}
}
if (filter.getBeginDate() != null) {
foundBeginDate = true;
if (foundState) {
query.append(" and");
}
query.append(" ");
query.append(TIME_COLUMN_NAME);
query.append(" > ");
query.append(getTimeFilter(filter.getBeginDate()));
query.append(" ");
}
if (filter.getEndDate() != null) {
if (foundState || foundBeginDate) {
query.append(" and");
}
query.append(" ");
query.append(TIME_COLUMN_NAME);
query.append(" < ");
query.append(getTimeFilter(filter.getEndDate()));
query.append(" ");
}
}
if (filter.getOrdering() == Ordering.DESCENDING) {
query.append(String.format(" ORDER BY %s DESC", TIME_COLUMN_NAME));
logger.debug("descending ordering ");
}
int limit = (filter.getPageNumber() + 1) * filter.getPageSize();
query.append(" limit " + limit);
logger.trace("appending limit {}", limit);
int totalEntriesAffected = ((filter.getPageNumber() + 1) * filter.getPageSize());
int startEntryNum = totalEntriesAffected
- (totalEntriesAffected - (filter.getPageSize() * filter.getPageNumber()));
logger.trace("startEntryNum {}", startEntryNum);
logger.debug("query string: {}", query.toString());
Query influxdbQuery = new Query(query.toString(), dbName);
List<Result> results = Collections.emptyList();
results = influxDB.query(influxdbQuery, timeUnit).getResults();
for (Result result : results) {
List<Series> seriess = result.getSeries();
if (result.getError() != null) {
logger.error(result.getError());
continue;
}
if (seriess == null) {
logger.debug("query returned no series");
} else {
for (Series series : seriess) {
logger.trace("series {}", series.toString());
String historicItemName = series.getName();
List<List<Object>> valuess = series.getValues();
if (valuess == null) {
logger.debug("query returned no values");
} else {
List<String> columns = series.getColumns();
logger.trace("columns {}", columns);
Integer timestampColumn = null;
Integer valueColumn = null;
for (int i = 0; i < columns.size(); i++) {
String columnName = columns.get(i);
if (columnName.equals(TIME_COLUMN_NAME)) {
timestampColumn = i;
} else if (columnName.equals(VALUE_COLUMN_NAME)) {
valueColumn = i;
}
}
if (valueColumn == null || timestampColumn == null) {
throw new RuntimeException("missing column");
}
for (int i = 0; i < valuess.size(); i++) {
Double rawTime = (Double) valuess.get(i).get(timestampColumn);
Date time = new Date(rawTime.longValue());
State value = objectToState(valuess.get(i).get(valueColumn), historicItemName);
logger.trace("adding historic item {}: time {} value {}", historicItemName, time, value);
historicItems.add(new InfluxdbItem(historicItemName, value, time));
}
}
}
}
}
return historicItems;
}
private String getTimeFilter(Date time) {
// for some reason we need to query using 'seconds' only
// passing milli seconds causes no results to be returned
long milliSeconds = time.getTime();
long seconds = milliSeconds / 1000;
return seconds + "s";
}
/**
* This method returns an integer if possible if not a double is returned. This is an optimization
* for influxdb because integers have less overhead.
*
* @param value the BigDecimal to be converted
* @return A double if possible else a double is returned.
*/
private Object convertBigDecimalToNum(BigDecimal value) {
Object convertedValue;
if (value.scale() == 0) {
logger.trace("found no fractional part");
convertedValue = value.toBigInteger();
} else {
logger.trace("found fractional part");
convertedValue = value.doubleValue();
}
return convertedValue;
}
/**
* Converts {@link State} to objects fitting into influxdb values.
*
* @param state to be converted
* @return integer or double value for DecimalType, 0 or 1 for OnOffType and OpenClosedType,
* integer for DateTimeType, String for all others
*/
private Object stateToObject(State state) {
Object value;
if (state instanceof HSBType) {
value = ((HSBType) state).toString();
logger.debug("got HSBType value {}", value);
} else if (state instanceof PointType) {
value = point2String((PointType) state);
logger.debug("got PointType value {}", value);
} else if (state instanceof DecimalType) {
value = convertBigDecimalToNum(((DecimalType) state).toBigDecimal());
logger.debug("got DecimalType value {}", value);
} else if (state instanceof OnOffType) {
value = (OnOffType) state == OnOffType.ON ? 1 : 0;
logger.debug("got OnOffType value {}", value);
} else if (state instanceof OpenClosedType) {
value = (OpenClosedType) state == OpenClosedType.OPEN ? 1 : 0;
logger.debug("got OpenClosedType value {}", value);
} else if (state instanceof DateTimeType) {
value = ((DateTimeType) state).getCalendar().getTime().getTime();
logger.debug("got DateTimeType value {}", value);
} else {
value = state.toString();
logger.debug("got String value {}", value);
}
return value;
}
/**
* Converts {@link State} to a String suitable for influxdb queries.
*
* @param state to be converted
* @return {@link String} equivalent of the {@link State}
*/
private String stateToString(State state) {
String value;
if (state instanceof DecimalType) {
value = ((DecimalType) state).toBigDecimal().toString();
} else if (state instanceof PointType) {
value = point2String((PointType) state);
} else if (state instanceof OnOffType) {
value = ((OnOffType) state) == OnOffType.ON ? DIGITAL_VALUE_ON : DIGITAL_VALUE_OFF;
} else if (state instanceof OpenClosedType) {
value = ((OpenClosedType) state) == OpenClosedType.OPEN ? DIGITAL_VALUE_ON : DIGITAL_VALUE_OFF;
} else if (state instanceof HSBType) {
value = ((HSBType) state).toString();
} else if (state instanceof DateTimeType) {
value = String.valueOf(((DateTimeType) state).getCalendar().getTime().getTime());
} else {
value = state.toString();
}
return value;
}
/**
* Converts a value to a {@link State} which is suitable for the given {@link Item}. This is
* needed for querying a {@link HistoricState}.
*
* @param value to be converted to a {@link State}
* @param itemName name of the {@link Item} to get the {@link State} for
* @return the state of the item represented by the itemName parameter, else the string value of
* the Object parameter
*/
private State objectToState(Object value, String itemName) {
String valueStr = String.valueOf(value);
if (itemRegistry != null) {
try {
Item item = itemRegistry.getItem(itemName);
if (item instanceof ColorItem) {
logger.debug("objectToState found a ColorItem {}", valueStr);
return new HSBType(valueStr);
} else if (item instanceof LocationItem) {
logger.debug("objectToState found a LocationItem");
return new PointType(valueStr);
} else if (item instanceof NumberItem) {
logger.debug("objectToState found a NumberItem");
return new DecimalType(valueStr);
} else if (item instanceof DimmerItem) {
logger.debug("objectToState found a DimmerItem");
return new PercentType(valueStr);
} else if (item instanceof SwitchItem) {
logger.debug("objectToState found a SwitchItem");
return string2DigitalValue(valueStr).equals(DIGITAL_VALUE_OFF) ? OnOffType.OFF : OnOffType.ON;
} else if (item instanceof ContactItem) {
logger.debug("objectToState found a ContactItem");
return (string2DigitalValue(valueStr).equals(DIGITAL_VALUE_OFF)) ? OpenClosedType.CLOSED
: OpenClosedType.OPEN;
} else if (item instanceof RollershutterItem) {
logger.debug("objectToState found a RollershutterItem");
return new PercentType(valueStr);
} else if (item instanceof DateTimeItem) {
logger.debug("objectToState found a DateItem");
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(new BigDecimal(valueStr).longValue());
return new DateTimeType(calendar);
} else {
logger.debug("objectToState found a other Item");
return new StringType(valueStr);
}
} catch (ItemNotFoundException e) {
logger.warn("Could not find item '{}' in registry", itemName);
}
}
// just return a StringType as a fallback
return new StringType(valueStr);
}
/**
* Maps a string value which expresses a {@link BigDecimal.ZERO } to DIGITAL_VALUE_OFF, all others
* to DIGITAL_VALUE_ON
*
* @param value to be mapped
* @return
*/
private String string2DigitalValue(String value) {
BigDecimal num = new BigDecimal(value);
if (num.compareTo(BigDecimal.ZERO) == 0) {
logger.trace("digitalvalue {}", DIGITAL_VALUE_OFF);
return DIGITAL_VALUE_OFF;
} else {
logger.trace("digitalvalue {}", DIGITAL_VALUE_ON);
return DIGITAL_VALUE_ON;
}
}
private String point2String(PointType point) {
StringBuffer buf = new StringBuffer();
buf.append(point.getLatitude().toString());
buf.append(",");
buf.append(point.getLongitude().toString());
if (point.getAltitude().equals(0)) {
buf.append(",");
buf.append(point.getAltitude().toString());
}
return buf.toString(); // latitude, longitude, altitude
}
}