[dbquery] Initial contribution (#8780)

* Initial commit

Intial work history lost due to the repository shrunk done at c53e4aed26 (intially started from old unshrunked repo)

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Implement reconnect attempts

If database can be connected at bridge initialization schedule retry attempts.
Prevent  query execution scheduling if bridge isn't online

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Minor documentation changes and fix trigger channel name

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Fix NPE bug initializing ThingActions

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Implement query actions and another fixes

Implement actions to execute query and get last query result
Correctly serialize as JSON non scalar results to result channels

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update parameters and correct channel

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Fix formatting and forgot part on previous commit

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Improve documentation

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Add javadoc comment and license to all classes

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Code cleanup

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Untrack unused i18n file

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Fix log level for query actions trace information

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Add dbquery addon to bundles pom

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Temporary remove mqtt bindings that make travis build to fail

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Fix formatting issue

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Revert "Temporary remove mqtt bindings that make travis build to fail"

This reverts commit 21c09957b5.

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Code clean up from static analysis

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update code to be compatible with 3.1.0

Update dependencies version
Update Copyright
Other minor changes for new static analysis validations.
Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Requested PR changes

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Apply suggestions from code review

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Suggestions from code review

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update parent version

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/jdbc-bridge.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Update bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* Changes asked in PR review

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Update bundles/org.openhab.binding.dbquery/README.md

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* README documentation imporovements

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

* Fix format issue

Signed-off-by: Joan Pujol <joanpujol@gmail.com>

Co-authored-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Joan Pujol 2021-10-17 14:33:18 +02:00 committed by GitHub
parent 642a2b79dc
commit 6cb5652955
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 3701 additions and 0 deletions

View File

@ -60,6 +60,7 @@
/bundles/org.openhab.binding.dali/ @rs22
/bundles/org.openhab.binding.danfossairunit/ @pravussum
/bundles/org.openhab.binding.darksky/ @cweitkamp
/bundles/org.openhab.binding.dbquery/ @lujop
/bundles/org.openhab.binding.deconz/ @openhab/add-ons-maintainers
/bundles/org.openhab.binding.denonmarantz/ @jwveldhuis
/bundles/org.openhab.binding.digiplex/ @rmichalak

View File

@ -286,6 +286,11 @@
<artifactId>org.openhab.binding.darksky</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.dbquery</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.deconz</artifactId>

View File

@ -0,0 +1,210 @@
# DBQuery Binding
This binding allows creating items from the result of native database queries.
It currently only supports InfluxDB 2.X.
You can use the addon in any situation where you want to create an item from a native query.
The source of the query can be any supported database, and doesn't need to be the one you use as the persistence service in openHAB.
Some use cases can be:
- Integrate a device that stores its data in a database
- Query derived data from you openHAB persistence, for example with Influx2 tasks you can process your data to create a new one
- Bypass limitations of current openHAB persistence queries
## Supported Things
There are two types of supported things: `influxdb2` and a `query`.
For each different database you want to connect to, you must define a `Bridge` thing for that database.
Then each `Bridge` can define as many `Query` things that you want to execute.
## Thing Configuration
### Bridges
#### influxdb2
Defines a connection to an Influx2 database and allows creating queries on it.
| Parameter | Required | Description |
|--------------|----------|----------------------------------------- |
| url | Yes | database url |
| user | Yes | name of the database user |
| token | Yes | token to authenticate to the database ([Intructions about how to create one](https://v2.docs.influxdata.com/v2.0/security/tokens/create-token/)) |
| organization | Yes | database organization name |
| bucket | Yes | database bucket name |
### query
The `Query` thing defines a native query that provides several channels that you can bind to items.
#### Query parameters
The query items support the following parameters:
| Parameter | Required | Default | Description |
|--------------|----------|----------|-----------------------------------------------------------------------|
| query | true | | Query string in native syntax |
| interval | false | 0 | Interval in seconds in which the query is automatically executed |
| hasParameters| false | false | True if the query has parameters, false otherwise |
| timeout | false | 0 | Query execution timeout in seconds |
| scalarResult | false | true | If query always returns a single value or not |
| scalarColumn | false | | In case of multiple columns, it indicates which to use for scalarResult|
These are described further in the following subsections.
##### query
The query the items represents in the native language of your database:
- Flux for `influxdb2`
#### hasParameters
If `hasParameters=true` you can use parameters in the query string that can be dynamically set with the `setQueryParameters` action.
For InfluxDB use the `${paramName}` syntax for each parameter, and keep in mind that the values from that parameters must be from a trusted source as current
parameter substitution is subject to query injection attacks.
#### timeout
A time-out in seconds to wait for the query result, if it's exceeded, the result will be discarded and the addon will do its best to cancel the query.
Currently it's ignored and it will be implemented in a future version.
#### scalarResult
If `true` the query is expected to return a single scalar value that will be available to `result` channels as string, number, boolean,...
If the query can return several rows and/or several columns per row then it needs to be set to `false` and the result can be retrieved in `resultString`
channel as JSON or using the `getLastQueryResult` action.
#### scalarColumn
In case `scalarResult` is `true` and the select returns multiple columns you can use that parameter to choose which column to use to extract the result.
## Channels
Query items offer the following channels to be able to query / bind them to items:
| Channel Type ID | Item Type | Description |
|-----------------|-----------|------------------------------------------------------------------------------------------------------------------------------------|
| execute | Switch | Send `ON` to execute the query manually. It also indicates if query is currently running (`ON`) or not running (`OFF`) |
| resultString | String | Result of last executed query as a String |
| resultNumber | Number | Result of last executed query as a Number, query must have `scalarResult=true` |
| resultDateTime | DateTime | Result of last executed query as a DateTime, query must have `scalarResult=true` |
| resultContact | Contact | Result of last executed query as Contact, query must have `scalarResult=true` |
| resultSwitch | Switch | Result of last executed query as Switch, query must have `scalarResult=true` |
| parameters | String | Contains parameters of last executed query as JSON|
| correct | Switch | `ON` if the last executed query completed successfully, `OFF` if the query failed.|
All the channels, except `execute`, are updated when the query execution finishes, and while there is a query in execution they have the values from
last previous executed query.
The `resultString` channel is the only valid one if `scalarResult=false`, and in that case it contains the query result serialized to JSON in that format:
{
correct : true,
data : [
{
column1 : value,
column2 : value
},
{ ... }, //row2
{ ... } //row3
]
}
### Channel Triggers
#### calculateParameters
Triggers when there's a need to calculate parameters before query execution.
When a query has `hasParameters=true` it fires the `calculateParameters` channel trigger and pauses the execution until `setQueryParameters` action is call in
that query.
In the case a query has parameters, it's expected that there is a rule that catches the `calculateParameters` trigger, calculate the parameters with the corresponding logic and then calls the `setQueryParameters` action, after that the query will be executed.
## Actions
### For DatabaseBridge
#### executeQuery
It allows executing a query synchronously from a script/rule without defining it in a Thing.
To execute the action you need to pass the following parameters:
- String query: The query to execute
- Map<String,Object>: Query parameters (empty map if not needed)
- int timeout: Query timeout in seconds
And it returns an `ActionQueryResult` that has the following properties:
- correct (boolean) : True if the query was executed correctly, false otherwise
- data (List<Map<String,Object>>): A list where each element is a row that is stored in a map with (columnName,value) entries
- isScalarResult: It returns if the result is scalar one (only one row with one column)
- resultAsScalar: It returns the result as a scalar if possible, if not returns null
Example (using Jython script):
from core.log import logging, LOG_PREFIX
log = logging.getLogger("{}.action_example".format(LOG_PREFIX))
map = {"time" : "-2h"}
influxdb = actions.get("dbquery","dbquery:influxdb2:sampleQuery") //Get bridge thing
result = influxdb.executeQuery("from(bucket: \"default\") |> range(start:-2h) |> filter(fn: (r) => r[\"_measurement\"] == \"go_memstats_frees_total\") |> filter(fn: (r) => r[\"_field\"] == \"counter\") |> mean()",{},5)
log.info("execute query result is "+str(result.data))
Use this action with care, because as the query is executed synchronously, it is not good to execute long-running queries that can block script execution.
### For Queries
#### setQueryParameters
It's used for queries with parameters to set them.
To execute the action you need to pass the parameters as a Map.
Example (using Jython script):
params = {"time" : "-2h"}
dbquery = actions.get("dbquery","dbquery:query:queryWithParams") //Get query thing
dbquery.setQueryParameters(params)
#### getLastQueryResult
It can be used in scripts to get the last query result.
It doesn't have any parameters and returns an `ActionQueryResult` as defined in `executeQuery` action.
Example (using Jython script):
dbquery = actions.get("dbquery","dbquery:query:queryWithParams") //Get query thing
result = dbquery.getLastQueryResult()
## Examples
### The Simplest case
Define a InfluxDB2 database thing and a query with an interval execution.
That executes the query every 15 seconds and punts the result in `myItem`.
# Bridge Thing definition
Bridge dbquery:influxdb2:mydatabase "InfluxDB2 Bridge" [ bucket="default", user="admin", url="http://localhost:8086", organization="openhab", token="*******" ]
# Query Thing definition
Thing dbquery:query:myquery "My Query" [ interval=15, hasParameters=false, scalarResult=true, timeout=0, query="from(bucket: \"default\") |> range(start:-1h) |> filter(fn: (r) => r[\"_measurement\"] == \"go_memstats_frees_total\") |> filter(fn: (r) => r[\"_field\"] == \"counter\") |> mean()", scalarColumn="_value" ]
# Item definition
Number myItem "QueryResult" {channel="dbquery:query:myquery:resultNumber"}
### A query with parameters
Using the previous example you change the `range(start:-1h)` for `range(start:${time})`
Create a rule that is fired
- **When** `calculateParameters` is triggered in `myquery`
- **Then** executes the following script action (in that example Jython):
map = {"time" : "-2h"}
dbquery = actions.get("dbquery","dbquery:query:myquery")
dbquery.setQueryParameters(map)

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.dbquery</artifactId>
<name>openHAB Add-ons :: Bundles :: DBQuery Binding</name>
<properties>
<bnd.importpackage>
!javax.annotation;!android.*,!com.android.*,!com.google.appengine.*,!dalvik.system,!kotlin.*,!kotlinx.*,!org.conscrypt,!sun.security.ssl,!org.apache.harmony.*,!org.apache.http.*,!rx.*,!org.msgpack.*
</bnd.importpackage>
</properties>
<dependencies>
<!-- influxdb-client-java -->
<dependency>
<groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<artifactId>influxdb-client-core</artifactId>
<groupId>com.influxdb</groupId>
<version>1.6.0</version>
</dependency>
<dependency>
<artifactId>converter-gson</artifactId>
<groupId>com.squareup.retrofit2</groupId>
<version>2.5.0</version>
</dependency>
<dependency>
<artifactId>converter-scalars</artifactId>
<groupId>com.squareup.retrofit2</groupId>
<version>2.5.0</version>
</dependency>
<dependency> <!-- also used for querydb library -->
<artifactId>gson</artifactId>
<groupId>com.google.code.gson</groupId>
<version>2.8.5</version>
</dependency>
<dependency>
<artifactId>gson-fire</artifactId>
<groupId>io.gsonfire</groupId>
<version>1.8.0</version>
</dependency>
<dependency>
<artifactId>okio</artifactId>
<groupId>com.squareup.okio</groupId>
<version>1.17.3</version>
</dependency>
<dependency>
<artifactId>commons-csv</artifactId>
<groupId>org.apache.commons</groupId>
<version>1.6</version>
</dependency>
<dependency>
<artifactId>json</artifactId>
<groupId>org.json</groupId>
<version>20180813</version>
</dependency>
<dependency>
<artifactId>okhttp</artifactId>
<groupId>com.squareup.okhttp3</groupId>
<version>3.14.4</version>
</dependency>
<dependency>
<artifactId>retrofit</artifactId>
<groupId>com.squareup.retrofit2</groupId>
<version>2.6.2</version>
</dependency>
<dependency>
<artifactId>jsr305</artifactId>
<groupId>com.google.code.findbugs</groupId>
<version>3.0.2</version>
</dependency>
<dependency>
<artifactId>logging-interceptor</artifactId>
<groupId>com.squareup.okhttp3</groupId>
<version>3.14.4</version>
</dependency>
<dependency>
<artifactId>rxjava</artifactId>
<groupId>io.reactivex.rxjava2</groupId>
<version>2.2.17</version>
</dependency>
<dependency>
<artifactId>reactive-streams</artifactId>
<groupId>org.reactivestreams</groupId>
<version>1.0.3</version>
</dependency>
<dependency>
<artifactId>swagger-annotations</artifactId>
<groupId>io.swagger</groupId>
<version>1.5.22</version>
</dependency>
<!-- end influxdb-client-java -->
</dependencies>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.dbquery-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-dbquery" description="DBQuery Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.dbquery/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.action;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Query result as it's exposed to users in thing actions
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class ActionQueryResult {
private final boolean correct;
private List<Map<String, @Nullable Object>> data = Collections.emptyList();
public ActionQueryResult(boolean correct, @Nullable List<Map<String, @Nullable Object>> data) {
this.correct = correct;
if (data != null) {
this.data = data;
}
}
public boolean isCorrect() {
return correct;
}
public List<Map<String, @Nullable Object>> getData() {
return data;
}
public @Nullable Object getResultAsScalar() {
var firstResult = data.get(0);
return isScalarResult() ? firstResult.get(firstResult.keySet().iterator().next()) : null;
}
public boolean isScalarResult() {
return data.size() == 1 && data.get(0).keySet().size() == 1;
}
}

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.action;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.DatabaseBridgeHandler;
import org.openhab.binding.dbquery.internal.QueryHandler;
import org.openhab.binding.dbquery.internal.domain.ExecuteNonConfiguredQuery;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.domain.ResultRow;
import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Joan Pujol - Initial contribution
*/
@ThingActionsScope(name = "dbquery")
@NonNullByDefault
public class DBQueryActions implements IDBQueryActions, ThingActions {
private final Logger logger = LoggerFactory.getLogger(DBQueryActions.class);
private @Nullable QueryHandler queryHandler;
private @Nullable DatabaseBridgeHandler databaseBridgeHandler;
@Override
@RuleAction(label = "Execute query", description = "Execute query synchronously (use with care)")
public ActionQueryResult executeQuery(String query, Map<String, @Nullable Object> parameters,
int timeoutInSeconds) {
logger.debug("executeQuery from action {} params={}", query, parameters);
var currentDatabaseBridgeHandler = databaseBridgeHandler;
if (currentDatabaseBridgeHandler != null) {
QueryResult queryResult = new ExecuteNonConfiguredQuery(currentDatabaseBridgeHandler.getDatabase())
.executeSynchronously(query, parameters, Duration.ofSeconds(timeoutInSeconds));
logger.debug("executeQuery from action result {}", queryResult);
return queryResult2ActionQueryResult(queryResult);
} else {
logger.warn("Execute queried ignored as databaseBridgeHandler is null");
return new ActionQueryResult(false, null);
}
}
private ActionQueryResult queryResult2ActionQueryResult(QueryResult queryResult) {
return new ActionQueryResult(queryResult.isCorrect(),
queryResult.getData().stream().map(DBQueryActions::resultRow2Map).collect(Collectors.toList()));
}
private static Map<String, @Nullable Object> resultRow2Map(ResultRow resultRow) {
Map<String, @Nullable Object> map = new HashMap<>();
for (String column : resultRow.getColumnNames()) {
map.put(column, resultRow.getValue(column));
}
return map;
}
@Override
@RuleAction(label = "Set query parameters", description = "Set query parameters for a query")
public void setQueryParameters(@ActionInput(name = "parameters") Map<String, @Nullable Object> parameters) {
logger.debug("setQueryParameters {}", parameters);
var queryHandler = getThingHandler();
if (queryHandler instanceof QueryHandler) {
((QueryHandler) queryHandler).setParameters(parameters);
} else {
logger.warn("setQueryParameters called on wrong Thing, it must be a Query Thing");
}
}
@Override
@RuleAction(label = "Get last query result", description = "Get last result from a query")
public ActionQueryResult getLastQueryResult() {
var currentQueryHandler = queryHandler;
if (currentQueryHandler != null) {
return queryResult2ActionQueryResult(queryHandler.getLastQueryResult());
} else {
logger.warn("getLastQueryResult ignored as queryHandler is null");
return new ActionQueryResult(false, null);
}
}
@Override
public void setThingHandler(ThingHandler thingHandler) {
if (thingHandler instanceof QueryHandler) {
this.queryHandler = ((QueryHandler) thingHandler);
} else if (thingHandler instanceof DatabaseBridgeHandler) {
this.databaseBridgeHandler = ((DatabaseBridgeHandler) thingHandler);
} else {
throw new UnnexpectedCondition("Not expected thing handler " + thingHandler);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return queryHandler != null ? queryHandler : databaseBridgeHandler;
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.action;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Defines rule actions for interacting with DBQuery addon Things.
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface IDBQueryActions {
ActionQueryResult executeQuery(String query, Map<String, @Nullable Object> parameters, int timeoutInSeconds);
ActionQueryResult getLastQueryResult();
void setQueryParameters(Map<String, @Nullable Object> parameters);
}

View File

@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Channel;
import org.openhab.core.types.State;
/**
* Abstract the operation to update a channel
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface ChannelStateUpdater {
void updateChannelState(Channel channelUID, State value);
}

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Channel;
/**
* Abstract the action to get channels that need to be updated
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface ChannelsToUpdateQueryResult {
List<Channel> getChannels();
}

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* Common constants, which are used across the whole binding.
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class DBQueryBindingConstants {
private static final String BINDING_ID = "dbquery";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_INFLUXDB2_BRIDGE = new ThingTypeUID(BINDING_ID, "influxdb2");
public static final ThingTypeUID THING_TYPE_QUERY = new ThingTypeUID(BINDING_ID, "query");
// List of all Channel ids
public static final String CHANNEL_EXECUTE = "execute";
public static final String CHANNEL_PARAMETERS = "parameters";
public static final String CHANNEL_CORRECT = "correct";
public static final String TRIGGER_CHANNEL_CALCULATE_PARAMETERS = "calculateParameters";
public static final String RESULT_STRING_CHANNEL_TYPE = "result-channel-string";
public static final String RESULT_NUMBER_CHANNEL_TYPE = "result-channel-number";
public static final String RESULT_DATETIME_CHANNEL_TYPE = "result-channel-datetime";
public static final String RESULT_CONTACT_CHANNEL_TYPE = "result-channel-contact";
public static final String RESULT_SWITCH_CHANNEL_TYPE = "result-channel-switch";
private DBQueryBindingConstants() {
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.THING_TYPE_INFLUXDB2_BRIDGE;
import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.THING_TYPE_QUERY;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* DBQuery binding factory that is responsible for creating things and thing handlers.
*
* @author Joan Pujol Espinar - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.dbquery", service = ThingHandlerFactory.class)
public class DBQueryHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_INFLUXDB2_BRIDGE,
THING_TYPE_QUERY);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_QUERY.equals(thingTypeUID)) {
return new QueryHandler(thing);
} else if (THING_TYPE_INFLUXDB2_BRIDGE.equals(thingTypeUID)) {
return new InfluxDB2BridgeHandler((Bridge) thing);
} else {
return null;
}
}
}

View File

@ -0,0 +1,125 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.action.DBQueryActions;
import org.openhab.binding.dbquery.internal.domain.Database;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base implementation common to all implementation of database bridge
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public abstract class DatabaseBridgeHandler extends BaseBridgeHandler {
private static final long RETRY_CONNECTION_ATTEMPT_TIME_SECONDS = 60;
private final Logger logger = LoggerFactory.getLogger(DatabaseBridgeHandler.class);
private Database database = Database.EMPTY;
private @Nullable ScheduledFuture<?> retryConnectionAttemptFuture;
public DatabaseBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
initConfig();
database = createDatabase();
connectDatabase();
}
private void connectDatabase() {
logger.debug("connectDatabase {}", database);
var completable = database.connect();
updateStatus(ThingStatus.UNKNOWN);
completable.thenAccept(result -> {
if (result) {
logger.trace("Succesfully connected to database {}", getThing().getUID());
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connect to database failed");
if (retryConnectionAttemptFuture == null) {
scheduleRetryConnectionAttempt();
}
}
});
}
protected void scheduleRetryConnectionAttempt() {
logger.trace("Scheduled retry connection attempt every {}", RETRY_CONNECTION_ATTEMPT_TIME_SECONDS);
retryConnectionAttemptFuture = scheduler.scheduleWithFixedDelay(this::connectDatabase,
RETRY_CONNECTION_ATTEMPT_TIME_SECONDS, RETRY_CONNECTION_ATTEMPT_TIME_SECONDS, TimeUnit.SECONDS);
}
protected abstract void initConfig();
@Override
public void dispose() {
cancelRetryConnectionAttemptIfPresent();
disconnectDatabase();
}
protected void cancelRetryConnectionAttemptIfPresent() {
ScheduledFuture<?> currentFuture = retryConnectionAttemptFuture;
if (currentFuture != null) {
currentFuture.cancel(true);
}
}
private void disconnectDatabase() {
var completable = database.disconnect();
updateStatus(ThingStatus.UNKNOWN);
completable.thenAccept(result -> {
if (result) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Successfully disconnected to database");
} else {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.COMMUNICATION_ERROR,
"Disconnect to database failed");
}
});
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// No commands supported
}
abstract Database createDatabase();
public Database getDatabase() {
return database;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(DBQueryActions.class);
}
}

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration;
import org.openhab.binding.dbquery.internal.dbimpl.influx2.Influx2Database;
import org.openhab.binding.dbquery.internal.dbimpl.influx2.InfluxDBClientFacadeImpl;
import org.openhab.binding.dbquery.internal.domain.Database;
import org.openhab.core.thing.Bridge;
/**
* Concrete implementation of {@link DatabaseBridgeHandler} for Influx2
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class InfluxDB2BridgeHandler extends DatabaseBridgeHandler {
private InfluxDB2BridgeConfiguration config = new InfluxDB2BridgeConfiguration();
public InfluxDB2BridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
Database createDatabase() {
return new Influx2Database(config, new InfluxDBClientFacadeImpl(config));
}
@Override
protected void initConfig() {
config = getConfig().as(InfluxDB2BridgeConfiguration.class);
}
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
/**
* Concrete implementation of {@link DatabaseBridgeHandler} for Influx2
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class JDBCBridgeHandler extends BaseBridgeHandler {
public JDBCBridgeHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
}

View File

@ -0,0 +1,92 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.openhab.binding.dbquery.internal.domain.Database;
import org.openhab.binding.dbquery.internal.domain.Query;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Mantains information of a query that is currently executing
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryExecution {
private final Logger logger = LoggerFactory.getLogger(QueryExecution.class);
private final Database database;
private final String queryString;
private final QueryConfiguration queryConfiguration;
private QueryParameters queryParameters;
private @Nullable QueryResultListener queryResultListener;
public QueryExecution(Database database, QueryConfiguration queryConfiguration,
QueryResultListener queryResultListener) {
this.database = database;
this.queryString = queryConfiguration.getQuery();
this.queryConfiguration = queryConfiguration;
this.queryResultListener = queryResultListener;
this.queryParameters = QueryParameters.EMPTY;
}
public void setQueryParameters(QueryParameters queryParameters) {
this.queryParameters = queryParameters;
}
public void execute() {
Query query;
if (queryConfiguration.isHasParameters()) {
query = database.queryFactory().createQuery(queryString, queryParameters, queryConfiguration);
} else {
query = database.queryFactory().createQuery(queryString, queryConfiguration);
}
logger.trace("Execute query {}", query);
database.executeQuery(query).thenAccept(this::notifyQueryResult).exceptionally(error -> {
logger.warn("Error executing query", error);
notifyQueryResult(QueryResult.ofIncorrectResult("Error executing query"));
return null;
});
}
private void notifyQueryResult(QueryResult queryResult) {
var currentQueryResultListener = queryResultListener;
if (currentQueryResultListener != null) {
currentQueryResultListener.queryResultReceived(queryResult);
}
}
public void cancel() {
queryResultListener = null;
}
public QueryParameters getQueryParameters() {
return queryParameters;
}
public interface QueryResultListener {
void queryResultReceived(QueryResult queryResult);
}
@Override
public String toString() {
return "QueryExecution{" + "queryString='" + queryString + '\'' + ", queryParameters=" + queryParameters + '}';
}
}

View File

@ -0,0 +1,270 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.CHANNEL_EXECUTE;
import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.TRIGGER_CHANNEL_CALCULATE_PARAMETERS;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.action.DBQueryActions;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.openhab.binding.dbquery.internal.domain.DBQueryJSONEncoder;
import org.openhab.binding.dbquery.internal.domain.Database;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.domain.QueryResultExtractor;
import org.openhab.binding.dbquery.internal.domain.ResultValue;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manages query thing, handling it's commands and updating it's channels
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(QueryHandler.class);
// Relax nullable rules as config can be only null when not initialized
private @NonNullByDefault({}) QueryConfiguration config;
private @NonNullByDefault({}) QueryResultExtractor queryResultExtractor;
private @Nullable ScheduledFuture<?> scheduledQueryExecutionInterval;
private @Nullable QueryResultChannelUpdater queryResultChannelUpdater;
private Database database = Database.EMPTY;
private final DBQueryJSONEncoder jsonEncoder = new DBQueryJSONEncoder();
private @Nullable QueryExecution currentQueryExecution;
private QueryResult lastQueryResult = QueryResult.NO_RESULT;
public QueryHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
config = getConfigAs(QueryConfiguration.class);
queryResultExtractor = new QueryResultExtractor(config);
initQueryResultChannelUpdater();
updateStateWithParentBridgeStatus();
}
private void initQueryResultChannelUpdater() {
ChannelStateUpdater channelStateUpdater = (channel, state) -> updateState(channel.getUID(), state);
queryResultChannelUpdater = new QueryResultChannelUpdater(channelStateUpdater, this::getResultChannels2Update);
}
private void scheduleQueryExecutionIntervalIfNeeded() {
int interval = config.getInterval();
if (interval != QueryConfiguration.NO_INTERVAL && scheduledQueryExecutionInterval == null) {
logger.trace("Scheduling query execution every {} seconds for {}", interval, getQueryIdentifier());
scheduledQueryExecutionInterval = scheduler.scheduleWithFixedDelay(this::executeQuery, 0, interval,
TimeUnit.SECONDS);
}
}
private ThingUID getQueryIdentifier() {
return getThing().getUID();
}
private void cancelQueryExecutionIntervalIfNeeded() {
ScheduledFuture<?> currentFuture = scheduledQueryExecutionInterval;
if (currentFuture != null) {
currentFuture.cancel(true);
scheduledQueryExecutionInterval = null;
}
}
@Override
public void dispose() {
cancelQueryExecutionIntervalIfNeeded();
cancelCurrentQueryExecution();
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("handleCommand for channel {} with command {}", channelUID, command);
if (command instanceof RefreshType) {
if (CHANNEL_EXECUTE.equals(channelUID.getId())) {
executeQuery();
}
} else {
logger.warn("Query Thing can only handle RefreshType commands as the thing is read-only");
}
}
private synchronized void executeQuery() {
if (getThing().getStatus() == ThingStatus.ONLINE) {
QueryExecution queryExecution = currentQueryExecution;
if (queryExecution != null) {
logger.debug("Previous query execution for {} discarded as a new one is requested",
getQueryIdentifier());
cancelCurrentQueryExecution();
}
queryExecution = new QueryExecution(database, config, queryResultReceived);
this.currentQueryExecution = queryExecution;
if (config.isHasParameters()) {
logger.trace("{} triggered to set parameters for {}", TRIGGER_CHANNEL_CALCULATE_PARAMETERS,
queryExecution);
updateParametersChannel(QueryParameters.EMPTY);
triggerChannel(TRIGGER_CHANNEL_CALCULATE_PARAMETERS);
} else {
queryExecution.execute();
}
} else {
logger.debug("Execute query ignored because thing status is {}", getThing().getStatus());
}
}
private synchronized void cancelCurrentQueryExecution() {
QueryExecution current = currentQueryExecution;
if (current != null) {
current.cancel();
currentQueryExecution = null;
}
}
private void updateStateWithQueryResult(QueryResult queryResult) {
var currentQueryResultChannelUpdater = queryResultChannelUpdater;
var localCurrentQueryExecution = this.currentQueryExecution;
lastQueryResult = queryResult;
if (currentQueryResultChannelUpdater != null && localCurrentQueryExecution != null) {
ResultValue resultValue = queryResultExtractor.extractResult(queryResult);
updateCorrectChannel(resultValue.isCorrect());
updateParametersChannel(localCurrentQueryExecution.getQueryParameters());
if (resultValue.isCorrect()) {
currentQueryResultChannelUpdater.updateChannelResults(resultValue.getResult());
} else {
currentQueryResultChannelUpdater.clearChannelResults();
}
} else {
logger.warn(
"QueryResult discarded as queryResultChannelUpdater nor currentQueryExecution are not expected to be null");
}
}
private void updateCorrectChannel(boolean correct) {
updateState(DBQueryBindingConstants.CHANNEL_CORRECT, OnOffType.from(correct));
}
private void updateParametersChannel(QueryParameters queryParameters) {
updateState(DBQueryBindingConstants.CHANNEL_PARAMETERS, new StringType(jsonEncoder.encode(queryParameters)));
}
private void updateStateWithParentBridgeStatus() {
final @Nullable Bridge bridge = getBridge();
DatabaseBridgeHandler databaseBridgeHandler;
if (bridge != null) {
@Nullable
BridgeHandler bridgeHandler = bridge.getHandler();
if (bridgeHandler instanceof DatabaseBridgeHandler) {
databaseBridgeHandler = (DatabaseBridgeHandler) bridgeHandler;
database = databaseBridgeHandler.getDatabase();
if (bridge.getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
@Override
protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
super.updateStatus(status, statusDetail, description);
if (status == ThingStatus.ONLINE) {
scheduleQueryExecutionIntervalIfNeeded();
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
cancelCurrentQueryExecution();
updateStateWithParentBridgeStatus();
}
public void setParameters(Map<String, @Nullable Object> parameters) {
final @Nullable QueryExecution queryExecution = currentQueryExecution;
if (queryExecution != null) {
QueryParameters queryParameters = new QueryParameters(parameters);
queryExecution.setQueryParameters(queryParameters);
queryExecution.execute();
} else {
logger.trace("setParameters ignored as there is any executing query for {}", getQueryIdentifier());
}
}
private final QueryExecution.QueryResultListener queryResultReceived = (QueryResult queryResult) -> {
synchronized (QueryHandler.this) {
logger.trace("queryResultReceived for {} : {}", getQueryIdentifier(), queryResult);
updateStateWithQueryResult(queryResult);
currentQueryExecution = null;
}
};
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(DBQueryActions.class);
}
public QueryResult getLastQueryResult() {
return lastQueryResult;
}
private List<Channel> getResultChannels2Update() {
return getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID()))
.filter(this::isResultChannel).collect(Collectors.toList());
}
private boolean isResultChannel(Channel channel) {
@Nullable
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
return channelTypeUID != null && channelTypeUID.getId().startsWith("result");
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Updates a query result to needed channels doing needed conversions
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryResultChannelUpdater {
private final ChannelStateUpdater channelStateUpdater;
private final ChannelsToUpdateQueryResult channels2Update;
private final Value2StateConverter value2StateConverter;
public QueryResultChannelUpdater(ChannelStateUpdater channelStateUpdater,
ChannelsToUpdateQueryResult channelsToUpdate) {
this.channelStateUpdater = channelStateUpdater;
this.channels2Update = channelsToUpdate;
this.value2StateConverter = new Value2StateConverter();
}
public void clearChannelResults() {
for (Channel channel : channels2Update.getChannels()) {
channelStateUpdater.updateChannelState(channel, UnDefType.NULL);
}
}
public void updateChannelResults(@Nullable Object extractedResult) {
for (Channel channel : channels2Update.getChannels()) {
Class<? extends State> targetType = calculateItemType(channel);
State state = value2StateConverter.convertValue(extractedResult, targetType);
channelStateUpdater.updateChannelState(channel, state);
}
}
private Class<? extends State> calculateItemType(Channel channel) {
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
String channelID = channelTypeUID != null ? channelTypeUID.getId()
: DBQueryBindingConstants.RESULT_STRING_CHANNEL_TYPE;
switch (channelID) {
case DBQueryBindingConstants.RESULT_STRING_CHANNEL_TYPE:
return StringType.class;
case DBQueryBindingConstants.RESULT_NUMBER_CHANNEL_TYPE:
return DecimalType.class;
case DBQueryBindingConstants.RESULT_DATETIME_CHANNEL_TYPE:
return DateTimeType.class;
case DBQueryBindingConstants.RESULT_SWITCH_CHANNEL_TYPE:
return OnOffType.class;
case DBQueryBindingConstants.RESULT_CONTACT_CHANNEL_TYPE:
return OpenClosedType.class;
default:
throw new UnnexpectedCondition("Unexpected channel type " + channelTypeUID);
}
}
}

View File

@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Date;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.domain.DBQueryJSONEncoder;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.error.UnnexpectedCondition;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Manage conversion from a value to needed State target type
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class Value2StateConverter {
private final Logger logger = LoggerFactory.getLogger(Value2StateConverter.class);
private final DBQueryJSONEncoder jsonEncoder = new DBQueryJSONEncoder();
public State convertValue(@Nullable Object value, Class<? extends State> targetType) {
if (value == null) {
return UnDefType.NULL;
} else {
if (targetType == StringType.class) {
return convert2String(value);
} else if (targetType == DecimalType.class) {
return convert2Decimal(value);
} else if (targetType == DateTimeType.class) {
return convert2DateTime(value);
} else if (targetType == OnOffType.class) {
@Nullable
Boolean bool = convert2Boolean(value);
return bool != null ? OnOffType.from(bool) : UnDefType.NULL;
} else if (targetType == OpenClosedType.class) {
@Nullable
Boolean bool = convert2Boolean(value);
if (bool != null) {
return bool ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
} else {
return UnDefType.NULL;
}
} else {
throw new UnnexpectedCondition("Not expected targetType " + targetType);
}
}
}
private State convert2DateTime(Object value) {
if (value instanceof Instant) {
return new DateTimeType(ZonedDateTime.ofInstant((Instant) value, ZoneId.systemDefault()));
} else if (value instanceof Date) {
return new DateTimeType(ZonedDateTime.ofInstant(((Date) value).toInstant(), ZoneId.systemDefault()));
} else if (value instanceof String) {
return new DateTimeType((String) value);
} else {
logger.warn("Can't convert {} to DateTimeType", value);
return UnDefType.NULL;
}
}
private State convert2Decimal(Object value) {
if (value instanceof Integer) {
return new DecimalType((Integer) value);
} else if (value instanceof Long) {
return new DecimalType((Long) value);
} else if (value instanceof Float) {
return new DecimalType((Float) value);
} else if (value instanceof Double) {
return new DecimalType((Double) value);
} else if (value instanceof BigDecimal) {
return new DecimalType((BigDecimal) value);
} else if (value instanceof BigInteger) {
return new DecimalType(new BigDecimal((BigInteger) value));
} else if (value instanceof Number) {
return new DecimalType(((Number) value).longValue());
} else if (value instanceof String) {
return DecimalType.valueOf((String) value);
} else if (value instanceof Duration) {
return new DecimalType(((Duration) value).toMillis());
} else {
logger.warn("Can't convert {} to DecimalType", value);
return UnDefType.NULL;
}
}
private State convert2String(Object value) {
if (value instanceof String) {
return new StringType((String) value);
} else if (value instanceof byte[]) {
return new StringType(Base64.getEncoder().encodeToString((byte[]) value));
} else if (value instanceof QueryResult) {
return new StringType(jsonEncoder.encode((QueryResult) value));
} else {
return new StringType(String.valueOf(value));
}
}
private @Nullable Boolean convert2Boolean(Object value) {
if (value instanceof Boolean) {
return (Boolean) value;
} else if (value instanceof Number) {
return ((Number) value).doubleValue() != 0d;
} else if (value instanceof String) {
var svalue = (String) value;
return Boolean.parseBoolean(svalue) || (svalue.equalsIgnoreCase("on")) || svalue.equals("1");
} else {
logger.warn("Can't convert {} to OnOffType or OpenClosedType", value);
return null;
}
}
}

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.config;
import java.util.StringJoiner;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains fields mapping InfluxDB2 bridge configuration parameters.
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class InfluxDB2BridgeConfiguration {
private String url;
private String user;
private String token;
private String bucket;
private String organization;
public InfluxDB2BridgeConfiguration(String url, String user, String token, String organization, String bucket) {
this.url = url;
this.user = user;
this.token = token;
this.organization = organization;
this.bucket = bucket;
}
public InfluxDB2BridgeConfiguration() {
// Used only when configuration is created by reflection using ConfigMapper
url = user = token = organization = bucket = "";
}
public String getUrl() {
return url;
}
public String getUser() {
return user;
}
public String getToken() {
return token;
}
public String getOrganization() {
return organization;
}
public String getBucket() {
return bucket;
}
@Override
public String toString() {
return new StringJoiner(", ", InfluxDB2BridgeConfiguration.class.getSimpleName() + "[", "]")
.add("url='" + url + "'").add("user='" + user + "'").add("token='" + "*".repeat(token.length()) + "'")
.add("organization='" + organization + "'").add("bucket='" + bucket + "'").toString();
}
}

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.config;
import java.util.StringJoiner;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Contains fields mapping query things parameters.
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryConfiguration {
public static final int NO_INTERVAL = 0;
private String query = "";
private int interval;
private int timeout;
private boolean scalarResult;
private boolean hasParameters;
private @Nullable String scalarColumn = "";
public QueryConfiguration() {
// Used only when configuration is created by reflection using ConfigMapper
}
public QueryConfiguration(String query, int interval, int timeout, boolean scalarResult,
@Nullable String scalarColumn, boolean hasParameters) {
this.query = query;
this.interval = interval;
this.timeout = timeout;
this.scalarResult = scalarResult;
this.scalarColumn = scalarColumn;
this.hasParameters = hasParameters;
}
public String getQuery() {
return query;
}
public int getInterval() {
return interval;
}
public int getTimeout() {
return timeout;
}
public boolean isScalarResult() {
return scalarResult;
}
public @Nullable String getScalarColumn() {
var currentScalarColumn = scalarColumn;
return currentScalarColumn != null ? currentScalarColumn : "";
}
public boolean isScalarColumnDefined() {
return !getScalarColumn().isBlank();
}
public boolean isHasParameters() {
return hasParameters;
}
@Override
public String toString() {
return new StringJoiner(", ", QueryConfiguration.class.getSimpleName() + "[", "]").add("query='" + query + "'")
.add("interval=" + interval).add("timeout=" + timeout).add("scalarResult=" + scalarResult)
.add("hasParameters=" + hasParameters).add("scalarColumn='" + scalarColumn + "'").toString();
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl;
import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
/**
* Provides a parser to substitute query parameters for database like InfluxDB that doesn't support that in it's client.
* It's not ideal because it's subject to query injection attacks but it does the work if params are from trusted
* sources.
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class StringSubstitutionParamsParser {
private final Pattern paramPattern = Pattern.compile("\\$\\{([\\w_]*?)}");
private final String query;
public StringSubstitutionParamsParser(String query) {
this.query = query;
}
public String getQueryWithParametersReplaced(QueryParameters queryParameters) {
var matcher = paramPattern.matcher(query);
int idx = 0;
StringBuilder substitutedQuery = new StringBuilder();
while (matcher.find()) {
String nonParametersPart = query.substring(idx, matcher.start());
String parameterName = matcher.group(1);
substitutedQuery.append(nonParametersPart);
substitutedQuery.append(parameterValue(parameterName, queryParameters));
idx = matcher.end();
}
if (idx < query.length()) {
substitutedQuery.append(query.substring(idx));
}
return substitutedQuery.toString();
}
private String parameterValue(String parameterName, QueryParameters queryParameters) {
var parameter = queryParameters.getParameter(parameterName);
if (parameter != null) {
return parameter.toString();
} else {
return "";
}
}
}

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration;
import org.openhab.binding.dbquery.internal.domain.Database;
import org.openhab.binding.dbquery.internal.domain.Query;
import org.openhab.binding.dbquery.internal.domain.QueryFactory;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.error.DatabaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.influxdb.query.FluxRecord;
/**
* Influx2 implementation of {@link Database}
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class Influx2Database implements Database {
private final Logger logger = LoggerFactory.getLogger(Influx2Database.class);
private final ExecutorService executors;
private final InfluxDB2BridgeConfiguration config;
private final InfluxDBClientFacade client;
private final QueryFactory queryFactory;
public Influx2Database(InfluxDB2BridgeConfiguration config, InfluxDBClientFacade influxDBClientFacade) {
this.config = config;
this.client = influxDBClientFacade;
executors = Executors.newSingleThreadScheduledExecutor();
queryFactory = new Influx2QueryFactory();
}
@Override
public boolean isConnected() {
return client.isConnected();
}
@Override
public CompletableFuture<Boolean> connect() {
return CompletableFuture.supplyAsync(() -> {
synchronized (Influx2Database.this) {
return client.connect();
}
}, executors);
}
@Override
public CompletableFuture<Boolean> disconnect() {
return CompletableFuture.supplyAsync(() -> {
synchronized (Influx2Database.this) {
return client.disconnect();
}
}, executors);
}
@Override
public QueryFactory queryFactory() throws DatabaseException {
return queryFactory;
}
@Override
public CompletableFuture<QueryResult> executeQuery(Query query) {
try {
if (query instanceof Influx2QueryFactory.Influx2Query) {
Influx2QueryFactory.Influx2Query influxQuery = (Influx2QueryFactory.Influx2Query) query;
CompletableFuture<QueryResult> asyncResult = new CompletableFuture<>();
List<FluxRecord> records = new ArrayList<>();
client.query(influxQuery.getQuery(), (cancellable, record) -> { // onNext
records.add(record);
}, error -> { // onError
logger.warn("Error executing query {}", query, error);
asyncResult.complete(QueryResult.ofIncorrectResult("Error executing query"));
}, () -> { // onComplete
asyncResult.complete(new Influx2QueryResultExtractor().apply(records));
});
return asyncResult;
} else {
return CompletableFuture
.completedFuture(QueryResult.ofIncorrectResult("Unnexpected query type " + query));
}
} catch (RuntimeException e) {
return CompletableFuture.failedFuture(e);
}
}
@Override
public String toString() {
return "Influx2Database{config=" + config + '}';
}
}

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.openhab.binding.dbquery.internal.dbimpl.StringSubstitutionParamsParser;
import org.openhab.binding.dbquery.internal.domain.Query;
import org.openhab.binding.dbquery.internal.domain.QueryFactory;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
/**
* Influx2 implementation of {@link QueryFactory}
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class Influx2QueryFactory implements QueryFactory {
@Override
public Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration) {
return new Influx2Query(query);
}
@Override
public Query createQuery(String query, QueryParameters parameters,
@Nullable QueryConfiguration queryConfiguration) {
return new Influx2Query(substituteParameters(query, parameters));
}
private String substituteParameters(String query, QueryParameters parameters) {
return new StringSubstitutionParamsParser(query).getQueryWithParametersReplaced(parameters);
}
static class Influx2Query implements Query {
private final String query;
public Influx2Query(String query) {
this.query = query;
}
String getQuery() {
return query;
}
@Override
public String toString() {
return query;
}
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.domain.ResultRow;
import com.influxdb.query.FluxRecord;
/**
* Extracts results from Influx2 client query result to a {@link QueryResult}
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class Influx2QueryResultExtractor implements Function<List<FluxRecord>, QueryResult> {
@Override
public QueryResult apply(List<FluxRecord> records) {
var rows = records.stream().map(Influx2QueryResultExtractor::mapRecord2Row).collect(Collectors.toList());
return QueryResult.of(rows);
}
private static ResultRow mapRecord2Row(FluxRecord record) {
Map<String, @Nullable Object> values = record.getValues().entrySet().stream()
.filter(entry -> !Set.of("result", "table").contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
return new ResultRow(values);
}
}

View File

@ -0,0 +1,38 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.influxdb.Cancellable;
import com.influxdb.query.FluxRecord;
/**
* Facade to Influx2 client to facilitate testing
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface InfluxDBClientFacade {
boolean connect();
boolean isConnected();
boolean disconnect();
void query(String query, BiConsumer<Cancellable, FluxRecord> onNext, Consumer<? super Throwable> onError,
Runnable onComplete);
}

View File

@ -0,0 +1,116 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.influxdb.Cancellable;
import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.InfluxDBClientOptions;
import com.influxdb.client.QueryApi;
import com.influxdb.client.domain.Ready;
import com.influxdb.query.FluxRecord;
/**
* Real implementation of {@link InfluxDBClientFacade}
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class InfluxDBClientFacadeImpl implements InfluxDBClientFacade {
private final Logger logger = LoggerFactory.getLogger(InfluxDBClientFacadeImpl.class);
private final InfluxDB2BridgeConfiguration config;
private @Nullable InfluxDBClient client;
private @Nullable QueryApi queryAPI;
public InfluxDBClientFacadeImpl(InfluxDB2BridgeConfiguration config) {
this.config = config;
}
@Override
public boolean connect() {
var clientOptions = InfluxDBClientOptions.builder().url(config.getUrl()).org(config.getOrganization())
.bucket(config.getBucket()).authenticateToken(config.getToken().toCharArray()).build();
final InfluxDBClient createdClient = InfluxDBClientFactory.create(clientOptions);
this.client = createdClient;
var currentQueryAPI = createdClient.getQueryApi();
this.queryAPI = currentQueryAPI;
boolean connected = checkConnectionStatus();
if (connected) {
logger.debug("Successfully connected to InfluxDB. Instance ready={}", createdClient.ready());
} else {
logger.warn("Not able to connect to InfluxDB with config {}", config);
}
return connected;
}
private boolean checkConnectionStatus() {
final InfluxDBClient currentClient = client;
if (currentClient != null) {
Ready ready = currentClient.ready();
boolean isUp = ready != null && ready.getStatus() == Ready.StatusEnum.READY;
if (isUp) {
logger.debug("database status is OK");
} else {
logger.warn("database not ready");
}
return isUp;
} else {
logger.warn("checkConnection: database is not connected");
return false;
}
}
@Override
public boolean isConnected() {
return checkConnectionStatus();
}
@Override
public boolean disconnect() {
final InfluxDBClient currentClient = client;
if (currentClient != null) {
currentClient.close();
client = null;
queryAPI = null;
logger.debug("Succesfully disconnected from InfluxDB");
} else {
logger.debug("Already disconnected");
}
return true;
}
@Override
public void query(String query, BiConsumer<Cancellable, FluxRecord> onNext, Consumer<? super Throwable> onError,
Runnable onComplete) {
var currentQueryAPI = queryAPI;
if (currentQueryAPI != null) {
currentQueryAPI.query(query, onNext, onError, onComplete);
} else {
logger.warn("Query ignored as current queryAPI is null");
}
}
}

View File

@ -0,0 +1,109 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.lang.reflect.Type;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
/**
* Encodes domain objects to JSON
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class DBQueryJSONEncoder {
private final Gson gson;
public DBQueryJSONEncoder() {
gson = new GsonBuilder().registerTypeAdapter(QueryResult.class, new QueryResultGSONSerializer())
.registerTypeAdapter(ResultRow.class, new ResultRowGSONSerializer())
.registerTypeAdapter(QueryParameters.class, new QueryParametersGSONSerializer()).create();
}
public String encode(QueryResult queryResult) {
return gson.toJson(queryResult);
}
public String encode(QueryParameters parameters) {
return gson.toJson(parameters);
}
@NonNullByDefault({})
private static class QueryResultGSONSerializer implements JsonSerializer<QueryResult> {
@Override
public JsonElement serialize(QueryResult src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("correct", src.isCorrect());
if (src.getErrorMessage() != null) {
jsonObject.addProperty("errorMessage", src.getErrorMessage());
}
jsonObject.add("data", context.serialize(src.getData()));
return jsonObject;
}
}
private static class ResultRowGSONSerializer implements JsonSerializer<ResultRow> {
@Override
public JsonElement serialize(ResultRow src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
for (String columnName : src.getColumnNames()) {
jsonObject.add(columnName, convertValueToJsonPrimitive(src.getValue(columnName)));
}
return jsonObject;
}
}
private static class QueryParametersGSONSerializer implements JsonSerializer<QueryParameters> {
@Override
public JsonElement serialize(QueryParameters src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject jsonObject = new JsonObject();
for (Map.Entry<String, @Nullable Object> param : src.getAll().entrySet()) {
jsonObject.add(param.getKey(), convertValueToJsonPrimitive(param.getValue()));
}
return jsonObject;
}
}
private static JsonElement convertValueToJsonPrimitive(@Nullable Object value) {
if (value instanceof Number) {
return new JsonPrimitive((Number) value);
} else if (value instanceof Boolean) {
return new JsonPrimitive((Boolean) value);
} else if (value instanceof Character) {
return new JsonPrimitive((Character) value);
} else if (value instanceof Date) {
return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format(((Date) value).toInstant()));
} else if (value instanceof Instant) {
return new JsonPrimitive(DateTimeFormatter.ISO_INSTANT.format((Instant) value));
} else if (value != null) {
return new JsonPrimitive(value.toString());
} else {
return JsonNull.INSTANCE;
}
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dbquery.internal.error.DatabaseException;
/**
* Abstracts database operations needed for query execution
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface Database {
boolean isConnected();
CompletableFuture<Boolean> connect();
CompletableFuture<Boolean> disconnect();
QueryFactory queryFactory() throws DatabaseException;
CompletableFuture<QueryResult> executeQuery(Query query);
Database EMPTY = new Database() {
@Override
public boolean isConnected() {
return false;
}
@Override
public CompletableFuture<Boolean> connect() {
return CompletableFuture.completedFuture(false);
}
@Override
public CompletableFuture<Boolean> disconnect() {
return CompletableFuture.completedFuture(false);
}
@Override
public QueryFactory queryFactory() {
return QueryFactory.EMPTY;
}
@Override
public CompletableFuture<QueryResult> executeQuery(Query query) {
return CompletableFuture.completedFuture(QueryResult.ofIncorrectResult("Empty database"));
}
};
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Executes a non defined query in given database
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class ExecuteNonConfiguredQuery {
private final Logger logger = LoggerFactory.getLogger(ExecuteNonConfiguredQuery.class);
private final Database database;
public ExecuteNonConfiguredQuery(Database database) {
this.database = database;
}
public CompletableFuture<QueryResult> execute(String queryString, Map<String, @Nullable Object> parameters,
Duration timeout) {
if (!database.isConnected()) {
return CompletableFuture.completedFuture(QueryResult.ofIncorrectResult("Database not connected"));
}
Query query = database.queryFactory().createQuery(queryString, new QueryParameters(parameters),
createConfiguration(queryString, timeout));
return database.executeQuery(query);
}
public QueryResult executeSynchronously(String queryString, Map<String, @Nullable Object> parameters,
Duration timeout) {
var completableFuture = execute(queryString, parameters, timeout);
try {
if (timeout.isZero()) {
return completableFuture.get();
} else {
return completableFuture.get(timeout.getSeconds(), TimeUnit.SECONDS);
}
} catch (InterruptedException e) {
logger.debug("Query was interrupted", e);
Thread.currentThread().interrupt();
return QueryResult.ofIncorrectResult("Query execution was interrupted");
} catch (ExecutionException e) {
logger.warn("Error executing query", e);
return QueryResult.ofIncorrectResult("Error executing query " + e.getMessage());
} catch (TimeoutException e) {
logger.debug("Timeout executing query", e);
return QueryResult.ofIncorrectResult("Timeout");
}
}
private QueryConfiguration createConfiguration(String query, Duration timeout) {
return new QueryConfiguration(query, QueryConfiguration.NO_INTERVAL, (int) timeout.getSeconds(), false, null,
true);
}
}

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Marker interface for queries
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface Query {
Query EMPTY = new Query() {
};
}

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
/**
* Abstracts operations needed to create a query from its thing configuration
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public interface QueryFactory {
Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration);
Query createQuery(String query, QueryParameters parameters, @Nullable QueryConfiguration queryConfiguration);
QueryFactory EMPTY = new QueryFactory() {
@Override
public Query createQuery(String query, @Nullable QueryConfiguration queryConfiguration) {
return Query.EMPTY;
}
@Override
public Query createQuery(String query, QueryParameters parameters,
@Nullable QueryConfiguration queryConfiguration) {
return Query.EMPTY;
}
};
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Query parameters
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryParameters {
public static final QueryParameters EMPTY = new QueryParameters(Collections.emptyMap());
private final Map<String, @Nullable Object> params;
private QueryParameters() {
this.params = new HashMap<>();
}
public QueryParameters(Map<String, @Nullable Object> params) {
this.params = params;
}
public void setParameter(String name, @Nullable Object value) {
params.put(name, value);
}
public @Nullable Object getParameter(String paramName) {
return params.get(paramName);
}
public Map<String, @Nullable Object> getAll() {
return Collections.unmodifiableMap(params);
}
public int size() {
return params.size();
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.util.Collections;
import java.util.List;
import java.util.StringJoiner;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Query result
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryResult {
public static final QueryResult NO_RESULT = QueryResult.ofIncorrectResult("No result");
private final boolean correct;
private final @Nullable String errorMessage;
private final List<ResultRow> data;
private QueryResult(boolean correct, String errorMessage) {
this.correct = correct;
this.errorMessage = errorMessage;
this.data = Collections.emptyList();
}
private QueryResult(List<ResultRow> data) {
this.correct = true;
this.errorMessage = null;
this.data = data;
}
public static QueryResult ofIncorrectResult(String errorMessage) {
return new QueryResult(false, errorMessage);
}
public static QueryResult of(ResultRow... rows) {
return new QueryResult(List.of(rows));
}
public static QueryResult of(List<ResultRow> rows) {
return new QueryResult(rows);
}
public static QueryResult ofSingleValue(String columnName, Object value) {
return new QueryResult(List.of(new ResultRow(columnName, value)));
}
public boolean isCorrect() {
return correct;
}
public @Nullable String getErrorMessage() {
return errorMessage;
}
public List<ResultRow> getData() {
return data;
}
@Override
public String toString() {
return new StringJoiner(", ", QueryResult.class.getSimpleName() + "[", "]").add("correct=" + correct)
.add("errorMessage='" + errorMessage + "'").add("data=" + data).toString();
}
}

View File

@ -0,0 +1,86 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extracts a result from {@link QueryResult} to a single value to be used in channels
* (after being converted that it's not responsability of this class)
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class QueryResultExtractor {
private final Logger logger = LoggerFactory.getLogger(QueryResultExtractor.class);
private final QueryConfiguration config;
public QueryResultExtractor(QueryConfiguration config) {
this.config = config;
}
public ResultValue extractResult(QueryResult queryResult) {
if (queryResult.isCorrect()) {
if (config.isScalarResult()) {
return getScalarValue(queryResult);
} else {
return ResultValue.of(queryResult);
}
} else {
return ResultValue.incorrect();
}
}
private ResultValue getScalarValue(QueryResult queryResult) {
if (validateHasScalarValue(queryResult)) {
var row = queryResult.getData().get(0);
@Nullable
Object value;
if (config.isScalarColumnDefined()) {
value = row.getValue(Objects.requireNonNull(config.getScalarColumn()));
} else {
value = row.getValue(row.getColumnNames().iterator().next());
}
return ResultValue.of(value);
} else {
return ResultValue.incorrect();
}
}
private boolean validateHasScalarValue(QueryResult queryResult) {
boolean valid = false;
String baseErrorMessage = "Can't get scalar value for result: ";
if (queryResult.isCorrect()) {
if (queryResult.getData().size() == 1) {
boolean oneColumn = queryResult.getData().get(0).getColumnsSize() == 1;
if (oneColumn || config.isScalarColumnDefined()) {
valid = true;
} else {
logger.warn("{} Columns size is {} and scalarColumn isn't defined", baseErrorMessage,
queryResult.getData().get(0).getColumnNames().size());
}
} else {
logger.warn("{} Rows size is {}", baseErrorMessage, queryResult.getData().size());
}
} else {
logger.debug("{} Incorrect result", baseErrorMessage);
}
return valid;
}
}

View File

@ -0,0 +1,79 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Query result row
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class ResultRow {
private final Logger logger = LoggerFactory.getLogger(ResultRow.class);
private final LinkedHashMap<String, @Nullable Object> values;
public ResultRow(String columnName, @Nullable Object value) {
this.values = new LinkedHashMap<>();
put(columnName, value);
}
public ResultRow(Map<String, @Nullable Object> values) {
this.values = new LinkedHashMap<>();
values.forEach(this::put);
}
public Set<String> getColumnNames() {
return values.keySet();
}
public int getColumnsSize() {
return values.size();
}
public @Nullable Object getValue(String column) {
return values.get(column);
}
public static boolean isValidResultRowType(@Nullable Object object) {
return object == null || object instanceof String || object instanceof Boolean || object instanceof Number
|| object instanceof byte[] || object instanceof Instant || object instanceof Date
|| object instanceof Duration;
}
private void put(String columnName, @Nullable Object value) {
if (!isValidResultRowType(value)) {
logger.trace("Value {} of type {} converted to String as not supported internal type in dbquery", value,
value.getClass());
value = value.toString();
}
values.put(columnName, value);
}
@Override
public String toString() {
return values.toString();
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A query result value as is extracted by {@link QueryResultExtractor} from a {@link QueryResult}
* to be set in channels
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class ResultValue {
private final boolean correct;
private final @Nullable Object result;
private ResultValue(boolean correct, @Nullable Object result) {
this.correct = correct;
this.result = result;
}
public static ResultValue of(@Nullable Object result) {
return new ResultValue(true, result);
}
public static ResultValue incorrect() {
return new ResultValue(false, null);
}
public boolean isCorrect() {
return correct;
}
public @Nullable Object getResult() {
return result;
}
}

View File

@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.error;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception from a database operation
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class DatabaseException extends RuntimeException {
private static final long serialVersionUID = 5181127643040903150L;
public DatabaseException(String message) {
super(message);
}
public DatabaseException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.error;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* An unexpected error, aka bug
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class UnnexpectedCondition extends RuntimeException {
private static final long serialVersionUID = -7785815761302340174L;
public UnnexpectedCondition(String message) {
super(message);
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="dbquery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>DBQuery Binding</name>
<description>This is the binding for DBQuery that allows to execute native database queries and bind their results to
items.</description>
</binding:binding>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dbquery"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="influxdb2">
<label>InfluxDB2 Bridge</label>
<description>The InfluxDB 2.0 represents a connection to a InfluxDB 2.0 server</description>
<config-description>
<parameter name="url" type="text" required="true">
<context>url</context>
<label>Url</label>
<description>Database url</description>
<default>http://localhost:9999</default>
</parameter>
<parameter name="user" type="text" required="true">
<label>Username</label>
<description>Name of the database user</description>
</parameter>
<parameter name="token" type="text" required="true">
<label>Token</label>
<context>password</context>
<description>Token to authenticate to the database</description>
</parameter>
<parameter name="organization" type="text" required="true">
<label>Organization</label>
<description>Name of the database organization </description>
</parameter>
<parameter name="bucket" type="text" required="true">
<label>Bucket</label>
<description>Name of the database bucket </description>
</parameter>
</config-description>
</bridge-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="dbquery"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="query">
<supported-bridge-type-refs>
<bridge-type-ref id="influxdb2"/>
</supported-bridge-type-refs>
<label>Query Thing</label>
<description>Thing that represents a native query</description>
<channels>
<channel id="execute" typeId="execute-channel"/>
<channel id="resultString" typeId="result-channel-string"/>
<channel id="resultNumber" typeId="result-channel-number"/>
<channel id="resultDateTime" typeId="result-channel-datetime"/>
<channel id="resultContact" typeId="result-channel-contact"/>
<channel id="resultSwitch" typeId="result-channel-switch"/>
<channel id="parameters" typeId="parameters-channel"/>
<channel id="correct" typeId="correct-channel"/>
<channel id="calculateParameters" typeId="calculate-parameters-channel"/>
</channels>
<config-description>
<parameter name="query" type="text" required="true">
<label>Query Definition</label>
<description>Query definition using native query language</description>
<context>script</context>
</parameter>
<parameter name="hasParameters" type="boolean">
<label>Query has Parameters</label>
<description>True if the query has parameters, otherwise false</description>
<default>false</default>
</parameter>
<parameter name="scalarResult" type="boolean">
<label>Scalar Result</label>
<description>True if the query always return only one single scalar value (only one row and one value-column in this
row), otherwise false</description>
<default>true</default>
</parameter>
<parameter name="scalarColumn" type="text" required="false">
<label>Scalar Column Name</label>
<description>The column's name that is used to extract scalarResult. If only one column is returned this
parameter
can be blank</description>
</parameter>
<parameter name="interval" type="integer" min="0">
<label>Interval</label>
<description>
An interval, in seconds, the query will be repeatedly executed. Default values is 0, which means that
query is never executed automatically. You need to send the ON command each time you wish to execute.
</description>
<default>0</default>
</parameter>
<parameter name="timeout" type="integer" min="0">
<label>Timeout Query</label>
<description>
A time-out in seconds to wait for the query result, if it's exceeded result will be discarded.
Use 0 for
no timeout
</description>
<default>0</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="execute-channel">
<item-type>Switch</item-type>
<label>Execute Query</label>
<description>Send ON to execute the query, the current state tells if the query is running</description>
</channel-type>
<channel-type id="result-channel-string">
<item-type>String</item-type>
<label>String Result</label>
<description>Execute query and binds result value to channel as a String</description>
</channel-type>
<channel-type id="result-channel-number">
<item-type>Number</item-type>
<label>Number Result</label>
<description>Execute query and binds result value to channel as a Number</description>
</channel-type>
<channel-type id="result-channel-datetime">
<item-type>DateTime</item-type>
<label>DateTime Result</label>
<description>Execute query and binds result value to channel as a DateTime</description>
</channel-type>
<channel-type id="result-channel-contact">
<item-type>DateTime</item-type>
<label>Contact Result</label>
<description>Execute query and binds result value to channel as a Contact</description>
</channel-type>
<channel-type id="result-channel-switch">
<item-type>Switch</item-type>
<label>Switch Result</label>
<description>Execute query and binds result value to channel as a Switch</description>
</channel-type>
<channel-type id="parameters-channel">
<item-type>String</item-type>
<label>JSON Result</label>
</channel-type>
<channel-type id="correct-channel">
<item-type>Switch</item-type>
<label>Last Query Worked</label>
<description>True if last query executed correctly</description>
</channel-type>
<channel-type id="calculate-parameters-channel">
<kind>trigger</kind>
<label>Calculate Parameters</label>
<description>Event to calculate query parameters</description>
<event>
<options>
<option value="START">START</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
import org.openhab.binding.dbquery.internal.domain.QueryResult;
import org.openhab.binding.dbquery.internal.domain.QueryResultExtractor;
import org.openhab.binding.dbquery.internal.domain.ResultRow;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
class Influx2QueryResultExtractorTest {
public static final QueryResult ONE_ROW_ONE_COLUMN_RESULT = QueryResult.ofSingleValue("AnyValueName", "value");
public static final QueryResult SEVERAL_ROWS_COLUMNS_RESULT = QueryResult.of(
new ResultRow(Map.of("valueName", "value1", "column2", "value2")),
new ResultRow(Map.of("valueName", "value1", "column2", "value2")));
public static final QueryResult ONE_ROW_SEVERAL_COLUMNS_RESULT = QueryResult
.of(new ResultRow(Map.of("valueName", "value1", "column2", "value2")));
public static final QueryResult INCORRECT_RESULT = QueryResult.ofIncorrectResult("Incorrect result");
private static final QueryConfiguration SCALAR_VALUE_CONFIG = new QueryConfiguration("query", 10, 10, true, null,
false);
private static final QueryConfiguration NON_SCALAR_VALUE_CONFIG = new QueryConfiguration("query", 10, 10, false,
null, false);
private static final QueryConfiguration SCALAR_VALUE_CONFIG_WITH_SCALAR_COLUMN = new QueryConfiguration("query", 10,
10, true, "valueName", false);
@Test
void givenAResultWithOneRowAndOneColumnAndScalarConfigurationScalarValueIsReturned() {
var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(ONE_ROW_ONE_COLUMN_RESULT);
assertThat(extracted.isCorrect(), is(Boolean.TRUE));
assertThat(extracted.getResult(), is("value"));
}
@Test
void givenAResultWithSeveralRowsAndScalarConfigurationIncorrectValueIsReturned() {
var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(SEVERAL_ROWS_COLUMNS_RESULT);
assertThat(extracted.isCorrect(), is(false));
assertThat(extracted.getResult(), nullValue());
}
@Test
void givenAResultWithSeveralColumnsAndScalarConfigurationIncorrectValueIsReturned() {
var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG).extractResult(ONE_ROW_SEVERAL_COLUMNS_RESULT);
assertThat(extracted.isCorrect(), is(false));
assertThat(extracted.getResult(), nullValue());
}
@Test
void givenAResultWithSeveralColumnsAndScalarConfigurationAndScalarColumnDefinedValueIsReturned() {
var extracted = new QueryResultExtractor(SCALAR_VALUE_CONFIG_WITH_SCALAR_COLUMN)
.extractResult(ONE_ROW_SEVERAL_COLUMNS_RESULT);
assertThat(extracted.isCorrect(), is(true));
assertThat(extracted.getResult(), is("value1"));
}
@Test
void givenAResultWithSeveralRowsAndNonScalarConfigQueryResultIsReturned() {
var extracted = new QueryResultExtractor(NON_SCALAR_VALUE_CONFIG).extractResult(SEVERAL_ROWS_COLUMNS_RESULT);
assertThat(extracted.isCorrect(), is(true));
assertThat(extracted.getResult(), is(SEVERAL_ROWS_COLUMNS_RESULT));
}
@Test
void givenAIncorrectResultIncorrectValueIsReturned() {
var extracted = new QueryResultExtractor(NON_SCALAR_VALUE_CONFIG).extractResult(INCORRECT_RESULT);
assertThat(extracted.isCorrect(), is(false));
assertThat(extracted.getResult(), nullValue());
}
}

View File

@ -0,0 +1,201 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.number.IsCloseTo.closeTo;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Base64;
import java.util.Date;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault({})
class Value2StateConverterTest {
public static final BigDecimal BIG_DECIMAL_NUMBER = new BigDecimal("212321213123123123123123");
private Value2StateConverter instance;
@BeforeEach
void setUp() {
instance = new Value2StateConverter();
}
@AfterEach
void tearDown() {
instance = null;
}
@ParameterizedTest
@ValueSource(classes = { StringType.class, DecimalType.class, DateTimeType.class, OpenClosedType.class,
OnOffType.class })
void givenNullValueReturnUndef(Class<State> classe) {
assertThat(instance.convertValue(null, classe), is(UnDefType.NULL));
}
@ParameterizedTest
@ValueSource(strings = { "", "stringValue" })
void givenStringValueAndStringTargetReturnStringtype(String value) {
var converted = instance.convertValue(value, StringType.class);
assertThat(converted.toFullString(), is(value));
}
@ParameterizedTest
@MethodSource("provideValuesOfAllSupportedResultRowTypesExceptBytes")
void givenValidObjectTypesAndStringTargetReturnStringtypeWithString(Object value) {
var converted = instance.convertValue(value, StringType.class);
assertThat(converted.toFullString(), is(value.toString()));
}
@Test
void givenByteArrayAndStringTargetReturnEncodedBase64() {
var someBytes = "Hello world".getBytes(Charset.defaultCharset());
var someBytesB64 = Base64.getEncoder().encodeToString(someBytes);
var converted = instance.convertValue(someBytes, StringType.class);
assertThat(converted.toFullString(), is(someBytesB64));
}
@ParameterizedTest
@MethodSource("provideNumericTypes")
void givenNumericTypeAndDecimalTargetReturnDecimaltype(Number value) {
var converted = instance.convertValue(value, DecimalType.class);
assertThat(converted, instanceOf(DecimalType.class));
assertThat(((DecimalType) converted).doubleValue(), closeTo(value.doubleValue(), 0.01d));
}
@ParameterizedTest
@MethodSource("provideNumericTypes")
void givenNumericStringAndDecimalTargetReturnDecimaltype(Number value) {
var numberString = value.toString();
var converted = instance.convertValue(numberString, DecimalType.class);
assertThat(converted, instanceOf(DecimalType.class));
assertThat(((DecimalType) converted).doubleValue(), closeTo(value.doubleValue(), 0.01d));
}
@Test
void givenDurationAndDecimalTargetReturnDecimaltypeWithMilliseconds() {
var duration = Duration.ofDays(1);
var converted = instance.convertValue(duration, DecimalType.class);
assertThat(converted, instanceOf(DecimalType.class));
assertThat(((DecimalType) converted).longValue(), is(duration.toMillis()));
}
@Test
void givenInstantAndDatetimeTargetReturnDatetype() {
var instant = Instant.now();
var converted = instance.convertValue(instant, DateTimeType.class);
assertThat(converted, instanceOf(DateTimeType.class));
assertThat(((DateTimeType) converted).getZonedDateTime(),
is(ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()).withFixedOffsetZone()));
}
@Test
void givenDateAndDatetimeTargetReturnDatetype() {
var date = new Date();
var converted = instance.convertValue(date, DateTimeType.class);
assertThat(converted, instanceOf(DateTimeType.class));
assertThat(((DateTimeType) converted).getZonedDateTime(),
is(ZonedDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()).withFixedOffsetZone()));
}
@ParameterizedTest
@ValueSource(strings = { "2019-10-12T07:20:50.52Z", "2019-10-12" })
void givenValidStringDateAndDatetimeTargetReturnDatetype(String date) {
var converted = instance.convertValue(date, DateTimeType.class);
assertThat(converted, instanceOf(DateTimeType.class));
var convertedDateTime = ((DateTimeType) converted).getZonedDateTime();
assertThat(convertedDateTime.getYear(), is(2019));
assertThat(convertedDateTime.getMonthValue(), is(10));
assertThat(convertedDateTime.getDayOfMonth(), is(12));
assertThat(convertedDateTime.getHour(), anyOf(is(7), is(0)));
}
@ParameterizedTest
@MethodSource("trueValues")
void givenValuesConsideratedTrueAndOnOffTargetReturnOn(Object value) {
var converted = instance.convertValue(value, OnOffType.class);
assertThat(converted, instanceOf(OnOffType.class));
assertThat(converted, is(OnOffType.ON));
}
@ParameterizedTest
@MethodSource("falseValues")
void givenValuesConsideratedFalseAndOnOffTargetReturnOff(Object value) {
var converted = instance.convertValue(value, OnOffType.class);
assertThat(converted, instanceOf(OnOffType.class));
assertThat(converted, is(OnOffType.OFF));
}
@ParameterizedTest
@MethodSource("trueValues")
void givenValuesConsideratedTrueAndOpenClosedTargetReturnOpen(Object value) {
var converted = instance.convertValue(value, OpenClosedType.class);
assertThat(converted, instanceOf(OpenClosedType.class));
assertThat(converted, is(OpenClosedType.OPEN));
}
@ParameterizedTest
@MethodSource("falseValues")
void givenValuesConsideratedFalseAndOpenClosedTargetReturnClosed(Object value) {
var converted = instance.convertValue(value, OpenClosedType.class);
assertThat(converted, instanceOf(OpenClosedType.class));
assertThat(converted, is(OpenClosedType.CLOSED));
}
private static Stream<Object> trueValues() {
return Stream.of("true", "True", 1, 2, "On", "on", -1, 0.3);
}
private static Stream<Object> falseValues() {
return Stream.of("false", "False", 0, 0.0d, "off", "Off", "", "a value");
}
private static Stream<Number> provideNumericTypes() {
return Stream.of(1L, 1.2, 1.2f, -1, 0, BIG_DECIMAL_NUMBER);
}
private static Stream<Object> provideValuesOfAllSupportedResultRowTypes() {
return Stream.of("", "String", Boolean.TRUE, 1L, 1.2, 1.2f, BIG_DECIMAL_NUMBER,
"bytes".getBytes(Charset.defaultCharset()), Instant.now(), new Date(), Duration.ofDays(1));
}
private static Stream<Object> provideValuesOfAllSupportedResultRowTypesExceptBytes() {
return provideValuesOfAllSupportedResultRowTypes().filter(o -> !(o instanceof byte[]));
}
}

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class StringSubstitutionParamsParserTest {
@Test
public void testMultipleParameters() {
String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> fill( value: ${fillValue})";
var parser = new StringSubstitutionParamsParser(query);
QueryParameters parameters = new QueryParameters(Map.of("start", "0", "fillValue", "1"));
var result = parser.getQueryWithParametersReplaced(parameters);
assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: 0) |> fill( value: 1)"));
}
@Test
public void testRepeatedParameter() {
String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> limit(n:${start})";
var parser = new StringSubstitutionParamsParser(query);
QueryParameters parameters = new QueryParameters(Map.of("start", "0"));
var result = parser.getQueryWithParametersReplaced(parameters);
assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: 0) |> limit(n:0)"));
}
@Test
public void testNullAndNotDefinedParametersAreSubstitutedByEmptyString() {
String query = "from(bucket:\\\"my-bucket\\\") |> range(start: ${start}) |> limit(n:${start})";
var parser = new StringSubstitutionParamsParser(query);
var paramMap = new HashMap<String, @Nullable Object>();
paramMap.put("start", null);
QueryParameters parameters = new QueryParameters(paramMap);
var result = parser.getQueryWithParametersReplaced(parameters);
assertThat(result, equalTo("from(bucket:\\\"my-bucket\\\") |> range(start: ) |> limit(n:)"));
}
}

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import org.eclipse.jdt.annotation.DefaultLocation;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.dbquery.internal.config.InfluxDB2BridgeConfiguration;
import org.openhab.binding.dbquery.internal.domain.Query;
import org.openhab.binding.dbquery.internal.domain.QueryParameters;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault(value = { DefaultLocation.PARAMETER })
class Influx2DatabaseTest {
private Influx2Database instance;
@BeforeEach
public void setup() {
instance = new Influx2Database(new InfluxDB2BridgeConfiguration(), new InfluxDBClientFacadeMock());
}
@AfterEach
public void clearDown() {
instance = null;
}
@Test
public void givenQueryThatReturnsScalarResultGetValidScalarResult() throws Exception {
instance.connect().get();
Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.SCALAR_QUERY, QueryParameters.EMPTY,
null);
var future = instance.executeQuery(query);
var queryResult = future.get();
assertThat(queryResult, notNullValue());
assertThat(queryResult.isCorrect(), is(true));
assertThat(queryResult.getData(), hasSize(1));
assertThat(queryResult.getData().get(0).getColumnsSize(), is(1));
}
@Test
public void givenQueryThatReturnsMultipleRowsGetValidQueryResult() throws Exception {
instance.connect().get();
Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.MULTIPLE_ROWS_QUERY,
QueryParameters.EMPTY, null);
var future = instance.executeQuery(query);
var queryResult = future.get();
assertThat(queryResult, notNullValue());
assertThat(queryResult.isCorrect(), is(true));
assertThat(queryResult.getData(), hasSize(InfluxDBClientFacadeMock.MULTIPLE_ROWS_SIZE));
assertThat("contains expected result data", queryResult.getData().stream().allMatch(row -> {
var value = (String) row.getValue(InfluxDBClientFacadeMock.VALUE_COLUMN);
var time = row.getValue(InfluxDBClientFacadeMock.TIME_COLUMN);
return value != null && value.contains(InfluxDBClientFacadeMock.MULTIPLE_ROWS_VALUE_PREFIX) && time != null;
}));
}
@Test
public void givenQueryThatReturnsErrorGetErroneusResult() throws Exception {
instance.connect().get();
Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.INVALID_QUERY, QueryParameters.EMPTY,
null);
var future = instance.executeQuery(query);
var queryResult = future.get();
assertThat(queryResult, notNullValue());
assertThat(queryResult.isCorrect(), equalTo(false));
assertThat(queryResult.getData(), is(empty()));
}
@Test
public void givenQueryThatReturnsNoRowsGetEmptyResult() throws Exception {
instance.connect().get();
Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.EMPTY_QUERY, QueryParameters.EMPTY,
null);
var future = instance.executeQuery(query);
var queryResult = future.get();
assertThat(queryResult, notNullValue());
assertThat(queryResult.isCorrect(), equalTo(true));
assertThat(queryResult.getData(), is(empty()));
}
@Test
public void givenNotConnectedClientShouldGetIncorrectQuery() {
Query query = instance.queryFactory().createQuery(InfluxDBClientFacadeMock.SCALAR_QUERY, QueryParameters.EMPTY,
null);
var future = instance.executeQuery(query);
assertThat(future.isCompletedExceptionally(), is(Boolean.TRUE));
}
}

View File

@ -0,0 +1,100 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.dbimpl.influx2;
import static org.mockito.Mockito.mock;
import java.time.Instant;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.dbquery.internal.error.DatabaseException;
import com.influxdb.Cancellable;
import com.influxdb.query.FluxRecord;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
public class InfluxDBClientFacadeMock implements InfluxDBClientFacade {
public static final String INVALID_QUERY = "invalid";
public static final String EMPTY_QUERY = "empty";
public static final String SCALAR_QUERY = "scalar";
public static final String MULTIPLE_ROWS_QUERY = "multiple";
public static final String SCALAR_RESULT = "scalarResult";
public static final int MULTIPLE_ROWS_SIZE = 3;
public static final String VALUE_COLUMN = "_value";
public static final String TIME_COLUMN = "_time";
public static final String MULTIPLE_ROWS_VALUE_PREFIX = "value";
boolean connected;
@Override
public boolean connect() {
connected = true;
return true;
}
@Override
public boolean isConnected() {
return connected;
}
@Override
public boolean disconnect() {
connected = false;
return true;
}
@Override
public void query(String queryString, BiConsumer<Cancellable, FluxRecord> onNext,
Consumer<? super Throwable> onError, Runnable onComplete) {
if (!connected) {
throw new DatabaseException("Client not connected");
}
if (INVALID_QUERY.equals(queryString)) {
onError.accept(new RuntimeException("Invalid query"));
} else if (EMPTY_QUERY.equals(queryString)) {
onComplete.run();
} else if (SCALAR_QUERY.equals(queryString)) {
FluxRecord scalar = new FluxRecord(0);
scalar.getValues().put("result", "_result");
scalar.getValues().put("table", 0);
scalar.getValues().put(VALUE_COLUMN, SCALAR_RESULT);
onNext.accept(mock(Cancellable.class), scalar);
onComplete.run();
} else if (MULTIPLE_ROWS_QUERY.equals(queryString)) {
onNext.accept(mock(Cancellable.class), createRowRecord(0, MULTIPLE_ROWS_VALUE_PREFIX + 1));
onNext.accept(mock(Cancellable.class), createRowRecord(0, MULTIPLE_ROWS_VALUE_PREFIX + 2));
onNext.accept(mock(Cancellable.class), createRowRecord(1, MULTIPLE_ROWS_VALUE_PREFIX + 3));
onComplete.run();
}
}
private static FluxRecord createRowRecord(int table, String value) {
FluxRecord record = new FluxRecord(0);
record.getValues().put("result", "_result");
record.getValues().put("table", table);
record.getValues().put(VALUE_COLUMN, value);
record.getValues().put(TIME_COLUMN, Instant.now());
record.getValues().put("_start", Instant.now());
record.getValues().put("_stop", Instant.now());
record.getValues().put("_measurement", "measurementName");
return record;
}
}

View File

@ -0,0 +1,142 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.dbquery.internal.domain;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.closeTo;
import static org.hamcrest.Matchers.lessThan;
import java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import com.google.gson.Gson;
import com.google.gson.JsonParser;
/**
*
* @author Joan Pujol - Initial contribution
*/
@NonNullByDefault
class QueryResultJSONEncoderTest {
public static final double TOLERANCE = 0.001d;
private final DBQueryJSONEncoder instance = new DBQueryJSONEncoder();
private final Gson gson = new Gson();
private final JsonParser jsonParser = new JsonParser();
@Test
void givenQueryResultIsSerializedToJson() {
String json = instance.encode(givenQueryResultWithResults());
assertThat(jsonParser.parse(json), notNullValue());
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void givenQueryResultItsContentIsCorrectlySerializedToJson() {
String json = instance.encode(givenQueryResultWithResults());
Map<String, Object> map = gson.fromJson(json, Map.class);
assertThat(map, Matchers.hasEntry("correct", Boolean.TRUE));
assertThat(map, Matchers.hasKey("data"));
List<Map> data = (List<Map>) map.get("data");
assertThat(data, Matchers.hasSize(2));
Map firstRow = data.get(0);
assertReadGivenValuesDecodedFromJson(firstRow);
}
private void assertReadGivenValuesDecodedFromJson(Map<?, ?> firstRow) {
assertThat(firstRow.get("strValue"), is("an string"));
Object doubleValue = firstRow.get("doubleValue");
assertThat(doubleValue, instanceOf(Number.class));
assertThat(((Number) doubleValue).doubleValue(), closeTo(2.3d, TOLERANCE));
Object intValue = firstRow.get("intValue");
assertThat(intValue, instanceOf(Number.class));
assertThat(((Number) intValue).intValue(), is(3));
Object longValue = firstRow.get("longValue");
assertThat(longValue, instanceOf(Number.class));
assertThat(((Number) longValue).longValue(), is(Long.MAX_VALUE));
Object date = Objects.requireNonNull(firstRow.get("date"));
assertThat(date, instanceOf(String.class));
var parsedDate = Instant.from(DateTimeFormatter.ISO_INSTANT.parse((String) date));
assertThat(Duration.between(parsedDate, Instant.now()).getSeconds(), lessThan(10L));
Object instant = Objects.requireNonNull(firstRow.get("instant"));
assertThat(instant, instanceOf(String.class));
var parsedInstant = Instant.from(DateTimeFormatter.ISO_INSTANT.parse((String) instant));
assertThat(Duration.between(parsedInstant, Instant.now()).getSeconds(), lessThan(10L));
assertThat(firstRow.get("booleanValue"), is(Boolean.TRUE));
assertThat(firstRow.get("object"), is("an object"));
}
@Test
@SuppressWarnings({ "unchecked" })
void givenQueryResultWithIncorrectResultItsContentIsCorrectlySerializedToJson() {
String json = instance.encode(QueryResult.ofIncorrectResult("Incorrect"));
Map<String, Object> map = gson.fromJson(json, Map.class);
assertThat(map, Matchers.hasEntry("correct", Boolean.FALSE));
assertThat(map.get("errorMessage"), is("Incorrect"));
}
@Test
void givenQueryParametersAreCorrectlySerializedToJson() {
QueryParameters queryParameters = new QueryParameters(givenRowValues());
String json = instance.encode(queryParameters);
Map<?, ?> map = Objects.requireNonNull(gson.fromJson(json, Map.class));
assertReadGivenValuesDecodedFromJson(map);
}
private QueryResult givenQueryResultWithResults() {
return QueryResult.of(new ResultRow(givenRowValues()), new ResultRow(givenRowValues()));
}
private Map<String, @Nullable Object> givenRowValues() {
Map<String, @Nullable Object> values = new HashMap<>();
values.put("strValue", "an string");
values.put("doubleValue", 2.3d);
values.put("intValue", 3);
values.put("longValue", Long.MAX_VALUE);
values.put("date", new Date());
values.put("instant", Instant.now());
values.put("booleanValue", Boolean.TRUE);
values.put("object", new Object() {
@Override
public String toString() {
return "an object";
}
});
return values;
}
}

View File

@ -92,6 +92,7 @@
<module>org.openhab.binding.dali</module>
<module>org.openhab.binding.danfossairunit</module>
<module>org.openhab.binding.darksky</module>
<module>org.openhab.binding.dbquery</module>
<module>org.openhab.binding.deconz</module>
<module>org.openhab.binding.denonmarantz</module>
<module>org.openhab.binding.digiplex</module>