diff --git a/CODEOWNERS b/CODEOWNERS
index 6589d06defe52..e45bb93ced9df 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 54c6f8c3dcdb3..bb8833638bc2a 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -286,6 +286,11 @@
org.openhab.binding.darksky
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.dbquery
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.deconz
diff --git a/bundles/org.openhab.binding.dbquery/README.md b/bundles/org.openhab.binding.dbquery/README.md
new file mode 100644
index 0000000000000..787bf06ac752f
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/README.md
@@ -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: 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>): 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)
diff --git a/bundles/org.openhab.binding.dbquery/pom.xml b/bundles/org.openhab.binding.dbquery/pom.xml
new file mode 100644
index 0000000000000..0ac0787c13860
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/pom.xml
@@ -0,0 +1,107 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.2.0-SNAPSHOT
+
+
+ org.openhab.binding.dbquery
+
+ openHAB Add-ons :: Bundles :: DBQuery Binding
+
+
+
+ !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.*
+
+
+
+
+
+
+ com.influxdb
+ influxdb-client-java
+ 1.6.0
+
+
+ influxdb-client-core
+ com.influxdb
+ 1.6.0
+
+
+ converter-gson
+ com.squareup.retrofit2
+ 2.5.0
+
+
+ converter-scalars
+ com.squareup.retrofit2
+ 2.5.0
+
+
+ gson
+ com.google.code.gson
+ 2.8.5
+
+
+ gson-fire
+ io.gsonfire
+ 1.8.0
+
+
+ okio
+ com.squareup.okio
+ 1.17.3
+
+
+ commons-csv
+ org.apache.commons
+ 1.6
+
+
+ json
+ org.json
+ 20180813
+
+
+ okhttp
+ com.squareup.okhttp3
+ 3.14.4
+
+
+ retrofit
+ com.squareup.retrofit2
+ 2.6.2
+
+
+ jsr305
+ com.google.code.findbugs
+ 3.0.2
+
+
+ logging-interceptor
+ com.squareup.okhttp3
+ 3.14.4
+
+
+ rxjava
+ io.reactivex.rxjava2
+ 2.2.17
+
+
+ reactive-streams
+ org.reactivestreams
+ 1.0.3
+
+
+ swagger-annotations
+ io.swagger
+ 1.5.22
+
+
+
+
diff --git a/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml b/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..23cd424abe4be
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.dbquery/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java
new file mode 100644
index 0000000000000..0973e6ba41dda
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/ActionQueryResult.java
@@ -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> data = Collections.emptyList();
+
+ public ActionQueryResult(boolean correct, @Nullable List> data) {
+ this.correct = correct;
+ if (data != null) {
+ this.data = data;
+ }
+ }
+
+ public boolean isCorrect() {
+ return correct;
+ }
+
+ public List> 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java
new file mode 100644
index 0000000000000..958c4d27eceed
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/DBQueryActions.java
@@ -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 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 resultRow2Map(ResultRow resultRow) {
+ Map 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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java
new file mode 100644
index 0000000000000..4a2fa70cc55cf
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/action/IDBQueryActions.java
@@ -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 parameters, int timeoutInSeconds);
+
+ ActionQueryResult getLastQueryResult();
+
+ void setQueryParameters(Map parameters);
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java
new file mode 100644
index 0000000000000..cbdb14efb3758
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelStateUpdater.java
@@ -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);
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java
new file mode 100644
index 0000000000000..68037abe42d2a
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/ChannelsToUpdateQueryResult.java
@@ -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 getChannels();
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java
new file mode 100644
index 0000000000000..a497b477d7997
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryBindingConstants.java
@@ -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() {
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java
new file mode 100644
index 0000000000000..5495dd34b4cf2
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DBQueryHandlerFactory.java
@@ -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 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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java
new file mode 100644
index 0000000000000..c2ffba22ee5ae
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/DatabaseBridgeHandler.java
@@ -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> getServices() {
+ return List.of(DBQueryActions.class);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java
new file mode 100644
index 0000000000000..984293ca0d073
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/InfluxDB2BridgeHandler.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java
new file mode 100644
index 0000000000000..58a741a03e9dd
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/JDBCBridgeHandler.java
@@ -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) {
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java
new file mode 100644
index 0000000000000..63fcde42fa1c1
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryExecution.java
@@ -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 + '}';
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java
new file mode 100644
index 0000000000000..652f25dd95a51
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryHandler.java
@@ -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 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> getServices() {
+ return List.of(DBQueryActions.class);
+ }
+
+ public QueryResult getLastQueryResult() {
+ return lastQueryResult;
+ }
+
+ private List 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");
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java
new file mode 100644
index 0000000000000..fe88f08aee4c6
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/QueryResultChannelUpdater.java
@@ -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);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java
new file mode 100644
index 0000000000000..6226ee2a4ae30
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/Value2StateConverter.java
@@ -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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java
new file mode 100644
index 0000000000000..c9a1fb6acc934
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/InfluxDB2BridgeConfiguration.java
@@ -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();
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java
new file mode 100644
index 0000000000000..48cff74033623
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/config/QueryConfiguration.java
@@ -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();
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java
new file mode 100644
index 0000000000000..0cd636c6bc4c5
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParser.java
@@ -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 "";
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java
new file mode 100644
index 0000000000000..cdbf1d99ad8c8
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2Database.java
@@ -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 connect() {
+ return CompletableFuture.supplyAsync(() -> {
+ synchronized (Influx2Database.this) {
+ return client.connect();
+ }
+ }, executors);
+ }
+
+ @Override
+ public CompletableFuture disconnect() {
+ return CompletableFuture.supplyAsync(() -> {
+ synchronized (Influx2Database.this) {
+ return client.disconnect();
+ }
+ }, executors);
+ }
+
+ @Override
+ public QueryFactory queryFactory() throws DatabaseException {
+ return queryFactory;
+ }
+
+ @Override
+ public CompletableFuture executeQuery(Query query) {
+ try {
+ if (query instanceof Influx2QueryFactory.Influx2Query) {
+ Influx2QueryFactory.Influx2Query influxQuery = (Influx2QueryFactory.Influx2Query) query;
+
+ CompletableFuture asyncResult = new CompletableFuture<>();
+ List 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 + '}';
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java
new file mode 100644
index 0000000000000..790a54582ea7d
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryFactory.java
@@ -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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java
new file mode 100644
index 0000000000000..2e16e00ae7eb7
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2QueryResultExtractor.java
@@ -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, QueryResult> {
+
+ @Override
+ public QueryResult apply(List records) {
+ var rows = records.stream().map(Influx2QueryResultExtractor::mapRecord2Row).collect(Collectors.toList());
+ return QueryResult.of(rows);
+ }
+
+ private static ResultRow mapRecord2Row(FluxRecord record) {
+ Map 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java
new file mode 100644
index 0000000000000..5dd9424f30472
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacade.java
@@ -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 onNext, Consumer super Throwable> onError,
+ Runnable onComplete);
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java
new file mode 100644
index 0000000000000..1bf86d8094ada
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeImpl.java
@@ -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 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");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java
new file mode 100644
index 0000000000000..c5bfb701317b4
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/DBQueryJSONEncoder.java
@@ -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 {
+ @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 {
+ @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 {
+ @Override
+ public JsonElement serialize(QueryParameters src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject jsonObject = new JsonObject();
+ for (Map.Entry 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;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java
new file mode 100644
index 0000000000000..58a170970ee32
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Database.java
@@ -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 connect();
+
+ CompletableFuture disconnect();
+
+ QueryFactory queryFactory() throws DatabaseException;
+
+ CompletableFuture executeQuery(Query query);
+
+ Database EMPTY = new Database() {
+ @Override
+ public boolean isConnected() {
+ return false;
+ }
+
+ @Override
+ public CompletableFuture connect() {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ @Override
+ public CompletableFuture disconnect() {
+ return CompletableFuture.completedFuture(false);
+ }
+
+ @Override
+ public QueryFactory queryFactory() {
+ return QueryFactory.EMPTY;
+ }
+
+ @Override
+ public CompletableFuture executeQuery(Query query) {
+ return CompletableFuture.completedFuture(QueryResult.ofIncorrectResult("Empty database"));
+ }
+ };
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java
new file mode 100644
index 0000000000000..a635cffcca8d5
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ExecuteNonConfiguredQuery.java
@@ -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 execute(String queryString, Map 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 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);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java
new file mode 100644
index 0000000000000..cad7bc8263246
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/Query.java
@@ -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() {
+ };
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java
new file mode 100644
index 0000000000000..a89ab676d41c7
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryFactory.java
@@ -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;
+ }
+ };
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java
new file mode 100644
index 0000000000000..617e12d95b079
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryParameters.java
@@ -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 params;
+
+ private QueryParameters() {
+ this.params = new HashMap<>();
+ }
+
+ public QueryParameters(Map 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 getAll() {
+ return Collections.unmodifiableMap(params);
+ }
+
+ public int size() {
+ return params.size();
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java
new file mode 100644
index 0000000000000..c3c4465372fcd
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResult.java
@@ -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 data;
+
+ private QueryResult(boolean correct, String errorMessage) {
+ this.correct = correct;
+ this.errorMessage = errorMessage;
+ this.data = Collections.emptyList();
+ }
+
+ private QueryResult(List 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 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 getData() {
+ return data;
+ }
+
+ @Override
+ public String toString() {
+ return new StringJoiner(", ", QueryResult.class.getSimpleName() + "[", "]").add("correct=" + correct)
+ .add("errorMessage='" + errorMessage + "'").add("data=" + data).toString();
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java
new file mode 100644
index 0000000000000..3ed8e1fa28c0d
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/QueryResultExtractor.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java
new file mode 100644
index 0000000000000..e404a2761381f
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultRow.java
@@ -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 values;
+
+ public ResultRow(String columnName, @Nullable Object value) {
+ this.values = new LinkedHashMap<>();
+ put(columnName, value);
+ }
+
+ public ResultRow(Map values) {
+ this.values = new LinkedHashMap<>();
+ values.forEach(this::put);
+ }
+
+ public Set 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();
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java
new file mode 100644
index 0000000000000..cc44f67863d80
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/domain/ResultValue.java
@@ -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;
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java
new file mode 100644
index 0000000000000..148853ee28b91
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/DatabaseException.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java
new file mode 100644
index 0000000000000..b558099777ccc
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/java/org/openhab/binding/dbquery/internal/error/UnnexpectedCondition.java
@@ -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);
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..89873ef13c70d
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ DBQuery Binding
+ This is the binding for DBQuery that allows to execute native database queries and bind their results to
+ items.
+
+
diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml
new file mode 100644
index 0000000000000..7ffc937f0d9d3
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/influx2-bridge.xml
@@ -0,0 +1,36 @@
+
+
+
+ InfluxDB2 Bridge
+ The InfluxDB 2.0 represents a connection to a InfluxDB 2.0 server
+
+
+
+ url
+ Url
+ Database url
+ http://localhost:9999
+
+
+ Username
+ Name of the database user
+
+
+ Token
+ password
+ Token to authenticate to the database
+
+
+ Organization
+ Name of the database organization
+
+
+ Bucket
+ Name of the database bucket
+
+
+
+
diff --git a/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..b4a71149e2570
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+ Query Thing
+ Thing that represents a native query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Query Definition
+ Query definition using native query language
+ script
+
+
+ Query has Parameters
+ True if the query has parameters, otherwise false
+ false
+
+
+ Scalar Result
+ True if the query always return only one single scalar value (only one row and one value-column in this
+ row), otherwise false
+ true
+
+
+ Scalar Column Name
+ The column's name that is used to extract scalarResult. If only one column is returned this
+ parameter
+ can be blank
+
+
+ Interval
+
+ 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.
+
+ 0
+
+
+ Timeout Query
+
+ A time-out in seconds to wait for the query result, if it's exceeded result will be discarded.
+ Use 0 for
+ no timeout
+
+ 0
+
+
+
+
+
+
+
+
+ Switch
+ Execute Query
+ Send ON to execute the query, the current state tells if the query is running
+
+
+ String
+ String Result
+ Execute query and binds result value to channel as a String
+
+
+ Number
+ Number Result
+ Execute query and binds result value to channel as a Number
+
+
+ DateTime
+ DateTime Result
+ Execute query and binds result value to channel as a DateTime
+
+
+ DateTime
+ Contact Result
+ Execute query and binds result value to channel as a Contact
+
+
+ Switch
+ Switch Result
+ Execute query and binds result value to channel as a Switch
+
+
+ String
+ JSON Result
+
+
+ Switch
+ Last Query Worked
+ True if last query executed correctly
+
+
+ trigger
+ Calculate Parameters
+ Event to calculate query parameters
+
+
+ START
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java
new file mode 100644
index 0000000000000..b1f653aedbc5a
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Influx2QueryResultExtractorTest.java
@@ -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());
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java
new file mode 100644
index 0000000000000..badca06273162
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/Value2StateConverterTest.java
@@ -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 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 trueValues() {
+ return Stream.of("true", "True", 1, 2, "On", "on", -1, 0.3);
+ }
+
+ private static Stream falseValues() {
+ return Stream.of("false", "False", 0, 0.0d, "off", "Off", "", "a value");
+ }
+
+ private static Stream provideNumericTypes() {
+ return Stream.of(1L, 1.2, 1.2f, -1, 0, BIG_DECIMAL_NUMBER);
+ }
+
+ private static Stream 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 provideValuesOfAllSupportedResultRowTypesExceptBytes() {
+ return provideValuesOfAllSupportedResultRowTypes().filter(o -> !(o instanceof byte[]));
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java
new file mode 100644
index 0000000000000..817d6d240ab7a
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/StringSubstitutionParamsParserTest.java
@@ -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();
+ 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:)"));
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java
new file mode 100644
index 0000000000000..17595fbded9ba
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/Influx2DatabaseTest.java
@@ -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));
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java
new file mode 100644
index 0000000000000..3e7cb255c744d
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/dbimpl/influx2/InfluxDBClientFacadeMock.java
@@ -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 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;
+ }
+}
diff --git a/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java
new file mode 100644
index 0000000000000..c99f59c1e3b9c
--- /dev/null
+++ b/bundles/org.openhab.binding.dbquery/src/test/java/org/openhab/binding/dbquery/internal/domain/QueryResultJSONEncoderTest.java
@@ -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 map = gson.fromJson(json, Map.class);
+ assertThat(map, Matchers.hasEntry("correct", Boolean.TRUE));
+ assertThat(map, Matchers.hasKey("data"));
+ List data = (List) 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 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 givenRowValues() {
+ Map 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;
+ }
+}
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 24e1feb55f47b..0fb3bffa4b80d 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -92,6 +92,7 @@
org.openhab.binding.dali
org.openhab.binding.danfossairunit
org.openhab.binding.darksky
+ org.openhab.binding.dbquery
org.openhab.binding.deconz
org.openhab.binding.denonmarantz
org.openhab.binding.digiplex