/** * 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.influxdb08.internal; 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.apache.commons.lang.StringUtils; import org.influxdb.InfluxDB; import org.influxdb.InfluxDBFactory; import org.influxdb.dto.Pong; import org.influxdb.dto.Serie; 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.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.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 0.8 {@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 * @author Ben Jones - Upgraded influxdb-java version * @author Dan Byers - Allow more item types to be handled * @since 1.5.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 OK_STATUS = "ok"; 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 Object TIME_COLUMN_NAME = "time"; private String dbName; private String url; private String user; private String password; 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) { disconnect(); if (config == null) { logger.warn("The configuration for influxdb08 is missing fix openhab.cfg"); } url = (String) config.get("url"); if (StringUtils.isBlank(url)) { url = DEFAULT_URL; logger.debug("using default url {}", DEFAULT_URL); } user = (String) config.get("user"); if (StringUtils.isBlank(user)) { user = DEFAULT_USER; logger.debug("using default user {}", DEFAULT_USER); } password = (String) config.get("password"); if (StringUtils.isBlank(password)) { logger.warn("The password is missing. To specify a password configure the password parameter in openhab.cfg."); } dbName = (String) config.get("db"); if (StringUtils.isBlank(dbName)) { dbName = DEFAULT_DB; logger.debug("using default db name {}", DEFAULT_DB); } 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(final int reason) { logger.debug("influxdb08 persistence service deactivated"); disconnect(); } private void connect() { if (influxDB == null) { // reuse an existing InfluxDB object because it has no state concerning the database // connection influxDB = InfluxDBFactory.connect(url, user, password); } 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(); if (pong.getStatus().equalsIgnoreCase(OK_STATUS)) { dbStatus = true; logger.debug("database status is OK"); } else { logger.error("database connection failed with status: \"{}\" response time was \"{}\"", pong.getStatus(), pong.getResponseTime()); dbStatus = false; } } catch (RuntimeException e) { dbStatus = false; logger.error("database connection failed throwing an exception"); handleDatabaseException(e); } } return dbStatus; } private void disconnect() { influxDB = null; connected = false; } private boolean isConnected() { return connected; } @Override public String getName() { return "influxdb08"; } @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 influxdb08 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 instanceof DimmerItem || item instanceof RollershutterItem) { state = item.getStateAs(PercentType.class); } else if (item instanceof ColorItem) { state = item.getStateAs(HSBType.class); } else { // All other items should return the best format by default state = item.getState(); } Object value = stateToObject(state); logger.trace("storing {} in influxdb08 {}", name, value); // For now time is calculated by influxdb08, may be this should be configurable? Serie serie = new Serie.Builder(name) .columns(VALUE_COLUMN_NAME) .values(value) .build(); // serie.setColumns(new String[] {"time", VALUE_COLUMN_NAME}); // Object[] point = new Object[] {System.currentTimeMillis(), value}; try { influxDB.write(dbName, TimeUnit.MILLISECONDS, serie); } 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 influxdb08 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 "); query.append(VALUE_COLUMN_NAME); query.append(", "); query.append(TIME_COLUMN_NAME); query.append(" "); query.append("from "); if (filter.getItemName() != null) { query.append(filter.getItemName()); } else { query.append("/.*/"); } logger.trace("filter itemname: {}", filter.getItemName()); logger.trace("filter ordering: {}", filter.getOrdering().toString()); logger.trace("filter state: {}", filter.getState()); logger.trace("filter operator: {}", filter.getOperator()); logger.trace("filter getBeginDate: {}", filter.getBeginDate()); logger.trace("filter getEndDate: {}", filter.getEndDate()); logger.trace("filter getPageSize: {}", filter.getPageSize()); logger.trace("filter getPageNumber: {}", 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(" "); } } // InfluxDB returns results in DESCENDING order by default // http://influxdb.com/docs/v0.7/api/query_language.html#select-and-time-ranges if (filter.getOrdering() == Ordering.ASCENDING) { query.append(" order asc"); } 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()); List<Serie> results = Collections.emptyList(); try { results = influxDB.query(dbName, query.toString(), TimeUnit.MILLISECONDS); } catch (RuntimeException e) { logger.error("query failed with database error"); handleDatabaseException(e); } for (Serie result : results) { String historicItemName = result.getName(); logger.trace("item name {}", historicItemName); int entryCount = 0; for (Map<String, Object> row : result.getRows()) { entryCount++; if (entryCount >= startEntryNum) { Double rawTime = (Double) row.get(TIME_COLUMN_NAME); Object rawValue = row.get(VALUE_COLUMN_NAME); logger.trace("adding historic item {}: time {} value {}", historicItemName, rawTime, rawValue); Date time = new Date(rawTime.longValue()); State value = objectToState(rawValue, historicItemName); historicItems.add(new InfluxdbItem(historicItemName, value, time)); } else { logger.trace("omitting item value for {}", historicItemName); } } } 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 DecimalType) { value = convertBigDecimalToNum(((DecimalType) state).toBigDecimal()); } else if (state instanceof OnOffType) { value = (OnOffType) state == OnOffType.ON ? 1 : 0; } else if (state instanceof OpenClosedType) { value = (OpenClosedType) state == OpenClosedType.OPEN ? 1 : 0; } else if (state instanceof HSBType) { value = ((HSBType) state).toString(); } else if (state instanceof DateTimeType) { value = ((DateTimeType) state).getCalendar().getTime().getTime(); } else { value = state.toString(); } 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 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 NumberItem) { return new DecimalType(valueStr); } else if (item instanceof ColorItem) { return new HSBType(valueStr); } else if (item instanceof DimmerItem) { return new PercentType(valueStr); } else if (item instanceof SwitchItem) { return string2DigitalValue(valueStr).equals(DIGITAL_VALUE_OFF) ? OnOffType.OFF : OnOffType.ON; } else if (item instanceof ContactItem) { return (string2DigitalValue(valueStr).equals(DIGITAL_VALUE_OFF)) ? OpenClosedType.CLOSED : OpenClosedType.OPEN; } else if (item instanceof RollershutterItem) { return new PercentType(valueStr); } else if (item instanceof DateTimeItem) { Calendar calendar = Calendar.getInstance(); calendar.setTimeInMillis(new BigDecimal(valueStr).longValue()); return new DateTimeType(calendar); } else { 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; } } }