2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.dbquery.internal;
15 import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.CHANNEL_EXECUTE;
16 import static org.openhab.binding.dbquery.internal.DBQueryBindingConstants.TRIGGER_CHANNEL_CALCULATE_PARAMETERS;
18 import java.util.Collection;
19 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.dbquery.action.DBQueryActions;
28 import org.openhab.binding.dbquery.internal.config.QueryConfiguration;
29 import org.openhab.binding.dbquery.internal.domain.DBQueryJSONEncoder;
30 import org.openhab.binding.dbquery.internal.domain.Database;
31 import org.openhab.binding.dbquery.internal.domain.QueryParameters;
32 import org.openhab.binding.dbquery.internal.domain.QueryResult;
33 import org.openhab.binding.dbquery.internal.domain.QueryResultExtractor;
34 import org.openhab.binding.dbquery.internal.domain.ResultValue;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.Channel;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.ThingStatusInfo;
44 import org.openhab.core.thing.ThingUID;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.BridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.thing.type.ChannelTypeUID;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * Manages query thing, handling it's commands and updating it's channels
57 * @author Joan Pujol - Initial contribution
60 public class QueryHandler extends BaseThingHandler {
62 private final Logger logger = LoggerFactory.getLogger(QueryHandler.class);
63 // Relax nullable rules as config can be only null when not initialized
64 private @NonNullByDefault({}) QueryConfiguration config;
65 private @NonNullByDefault({}) QueryResultExtractor queryResultExtractor;
67 private @Nullable ScheduledFuture<?> scheduledQueryExecutionInterval;
68 private @Nullable QueryResultChannelUpdater queryResultChannelUpdater;
69 private Database database = Database.EMPTY;
70 private final DBQueryJSONEncoder jsonEncoder = new DBQueryJSONEncoder();
72 private @Nullable QueryExecution currentQueryExecution;
73 private QueryResult lastQueryResult = QueryResult.NO_RESULT;
75 public QueryHandler(Thing thing) {
80 public void initialize() {
81 config = getConfigAs(QueryConfiguration.class);
82 queryResultExtractor = new QueryResultExtractor(config);
84 initQueryResultChannelUpdater();
85 updateStateWithParentBridgeStatus();
88 private void initQueryResultChannelUpdater() {
89 ChannelStateUpdater channelStateUpdater = (channel, state) -> updateState(channel.getUID(), state);
90 queryResultChannelUpdater = new QueryResultChannelUpdater(channelStateUpdater, this::getResultChannels2Update);
93 private void scheduleQueryExecutionIntervalIfNeeded() {
94 int interval = config.getInterval();
95 if (interval != QueryConfiguration.NO_INTERVAL && scheduledQueryExecutionInterval == null) {
96 logger.trace("Scheduling query execution every {} seconds for {}", interval, getQueryIdentifier());
97 scheduledQueryExecutionInterval = scheduler.scheduleWithFixedDelay(this::executeQuery, 0, interval,
102 private ThingUID getQueryIdentifier() {
103 return getThing().getUID();
106 private void cancelQueryExecutionIntervalIfNeeded() {
107 ScheduledFuture<?> currentFuture = scheduledQueryExecutionInterval;
108 if (currentFuture != null) {
109 currentFuture.cancel(true);
110 scheduledQueryExecutionInterval = null;
115 public void dispose() {
116 cancelQueryExecutionIntervalIfNeeded();
117 cancelCurrentQueryExecution();
122 public void handleCommand(ChannelUID channelUID, Command command) {
123 logger.trace("handleCommand for channel {} with command {}", channelUID, command);
125 if (command instanceof RefreshType) {
126 if (CHANNEL_EXECUTE.equals(channelUID.getId())) {
130 logger.warn("Query Thing can only handle RefreshType commands as the thing is read-only");
134 private synchronized void executeQuery() {
135 if (getThing().getStatus() == ThingStatus.ONLINE) {
136 QueryExecution queryExecution = currentQueryExecution;
137 if (queryExecution != null) {
138 logger.debug("Previous query execution for {} discarded as a new one is requested",
139 getQueryIdentifier());
140 cancelCurrentQueryExecution();
143 queryExecution = new QueryExecution(database, config, queryResultReceived);
144 this.currentQueryExecution = queryExecution;
146 if (config.isHasParameters()) {
147 logger.trace("{} triggered to set parameters for {}", TRIGGER_CHANNEL_CALCULATE_PARAMETERS,
149 updateParametersChannel(QueryParameters.EMPTY);
150 triggerChannel(TRIGGER_CHANNEL_CALCULATE_PARAMETERS);
152 queryExecution.execute();
155 logger.debug("Execute query ignored because thing status is {}", getThing().getStatus());
159 private synchronized void cancelCurrentQueryExecution() {
160 QueryExecution current = currentQueryExecution;
161 if (current != null) {
163 currentQueryExecution = null;
167 private void updateStateWithQueryResult(QueryResult queryResult) {
168 var currentQueryResultChannelUpdater = queryResultChannelUpdater;
169 var localCurrentQueryExecution = this.currentQueryExecution;
170 lastQueryResult = queryResult;
171 if (currentQueryResultChannelUpdater != null && localCurrentQueryExecution != null) {
172 ResultValue resultValue = queryResultExtractor.extractResult(queryResult);
173 updateCorrectChannel(resultValue.isCorrect());
174 updateParametersChannel(localCurrentQueryExecution.getQueryParameters());
175 if (resultValue.isCorrect()) {
176 currentQueryResultChannelUpdater.updateChannelResults(resultValue.getResult());
178 currentQueryResultChannelUpdater.clearChannelResults();
182 "QueryResult discarded as queryResultChannelUpdater nor currentQueryExecution are not expected to be null");
186 private void updateCorrectChannel(boolean correct) {
187 updateState(DBQueryBindingConstants.CHANNEL_CORRECT, OnOffType.from(correct));
190 private void updateParametersChannel(QueryParameters queryParameters) {
191 updateState(DBQueryBindingConstants.CHANNEL_PARAMETERS, new StringType(jsonEncoder.encode(queryParameters)));
194 private void updateStateWithParentBridgeStatus() {
195 final @Nullable Bridge bridge = getBridge();
197 if (bridge != null) {
199 BridgeHandler bridgeHandler = bridge.getHandler();
200 if (bridgeHandler instanceof DatabaseBridgeHandler databaseBridgeHandler) {
201 database = databaseBridgeHandler.getDatabase();
202 if (bridge.getStatus() == ThingStatus.ONLINE) {
203 updateStatus(ThingStatus.ONLINE);
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
216 protected void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
217 super.updateStatus(status, statusDetail, description);
218 if (status == ThingStatus.ONLINE) {
219 scheduleQueryExecutionIntervalIfNeeded();
224 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
225 cancelCurrentQueryExecution();
226 updateStateWithParentBridgeStatus();
229 public void setParameters(Map<String, @Nullable Object> parameters) {
230 final @Nullable QueryExecution queryExecution = currentQueryExecution;
231 if (queryExecution != null) {
232 QueryParameters queryParameters = new QueryParameters(parameters);
233 queryExecution.setQueryParameters(queryParameters);
234 queryExecution.execute();
236 logger.trace("setParameters ignored as there is any executing query for {}", getQueryIdentifier());
240 private final QueryExecution.QueryResultListener queryResultReceived = (QueryResult queryResult) -> {
241 synchronized (QueryHandler.this) {
242 logger.trace("queryResultReceived for {} : {}", getQueryIdentifier(), queryResult);
243 updateStateWithQueryResult(queryResult);
245 currentQueryExecution = null;
250 public Collection<Class<? extends ThingHandlerService>> getServices() {
251 return List.of(DBQueryActions.class);
254 public QueryResult getLastQueryResult() {
255 return lastQueryResult;
258 private List<Channel> getResultChannels2Update() {
259 return getThing().getChannels().stream().filter(channel -> isLinked(channel.getUID()))
260 .filter(this::isResultChannel).collect(Collectors.toList());
263 private boolean isResultChannel(Channel channel) {
265 ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
266 return channelTypeUID != null && channelTypeUID.getId().startsWith("result");