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.dali.internal.handler;
15 import static org.openhab.binding.dali.internal.DaliBindingConstants.*;
17 import java.io.DataInputStream;
18 import java.io.DataOutputStream;
19 import java.io.IOException;
20 import java.lang.reflect.InvocationTargetException;
21 import java.net.Socket;
22 import java.net.SocketTimeoutException;
24 import java.util.concurrent.CompletableFuture;
25 import java.util.concurrent.ExecutorService;
26 import java.util.concurrent.Executors;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.dali.internal.protocol.DaliBackwardFrame;
31 import org.openhab.binding.dali.internal.protocol.DaliCommandBase;
32 import org.openhab.binding.dali.internal.protocol.DaliResponse;
33 import org.openhab.core.common.NamedThreadFactory;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.ThingTypeUID;
39 import org.openhab.core.thing.binding.BaseBridgeHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.util.HexUtils;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * The {@link DaliserverBridgeHandler} handles the lifecycle of daliserver connections.
48 * @author Robert Schmid - Initial contribution
51 public class DaliserverBridgeHandler extends BaseBridgeHandler {
52 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(BRIDGE_TYPE);
54 private final Logger logger = LoggerFactory.getLogger(DaliserverBridgeHandler.class);
55 private static final int DALI_DEFAULT_TIMEOUT = 5000;
57 private DaliserverConfig config = new DaliserverConfig();
58 private @Nullable ExecutorService commandExecutor;
60 public DaliserverBridgeHandler(Bridge thing) {
65 public void handleCommand(ChannelUID channelUID, Command command) {
69 public void initialize() {
70 config = getConfigAs(DaliserverConfig.class);
71 commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
72 updateStatus(ThingStatus.ONLINE);
75 private Socket getConnection() throws IOException {
77 logger.debug("Creating connection to daliserver on: {} port: {}", config.host, config.port);
78 Socket socket = new Socket(config.host, config.port);
79 socket.setSoTimeout(DALI_DEFAULT_TIMEOUT);
81 } catch (IOException e) {
82 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
88 public void dispose() {
89 if (commandExecutor != null) {
90 commandExecutor.shutdownNow();
94 public CompletableFuture<@Nullable Void> sendCommand(DaliCommandBase command) {
95 return sendCommandWithResponse(command, DaliResponse.class).thenApply(c -> (Void) null);
98 public <T extends DaliResponse> CompletableFuture<@Nullable T> sendCommandWithResponse(DaliCommandBase command,
99 Class<T> responseType) {
100 CompletableFuture<@Nullable T> future = new CompletableFuture<>();
101 ExecutorService commandExecutor = this.commandExecutor;
102 if (commandExecutor != null) {
103 commandExecutor.submit(() -> {
104 byte[] prefix = new byte[] { 0x2, 0x0 };
105 byte[] message = command.frame.pack();
106 byte[] frame = new byte[prefix.length + message.length];
107 System.arraycopy(prefix, 0, frame, 0, prefix.length);
108 System.arraycopy(message, 0, frame, prefix.length, message.length);
110 try (Socket socket = getConnection();
111 DataOutputStream out = new DataOutputStream(socket.getOutputStream());
112 DataInputStream in = new DataInputStream(socket.getInputStream())) {
114 if (logger.isDebugEnabled()) {
115 logger.debug("Sending: {}", HexUtils.bytesToHex(frame));
118 if (command.sendTwice) {
120 in.readNBytes(4); // discard
128 T response = parseResponse(in, responseType);
129 future.complete(response);
131 } catch (DaliException e) {
132 future.completeExceptionally(e);
135 } catch (SocketTimeoutException e) {
136 logger.warn("Timeout sending command to daliserver: {} Message: {}", frame, e.getMessage());
137 future.completeExceptionally(new DaliException("Timeout sending command to daliserver", e));
138 } catch (IOException e) {
139 logger.warn("Problem sending command to daliserver: {} Message: {}", frame, e.getMessage());
140 future.completeExceptionally(new DaliException("Problem sending command to daliserver", e));
141 } catch (Exception e) {
142 logger.warn("Unexpected exception while sending command to daliserver: {} Message: {}", frame,
144 logger.trace("Stacktrace", e);
145 future.completeExceptionally(e);
149 future.complete(null);
154 private <T extends DaliResponse> @Nullable T parseResponse(DataInputStream reader, Class<T> responseType)
155 throws IOException, DaliException {
157 T result = responseType.getDeclaredConstructor().newInstance();
158 byte[] response = reader.readNBytes(4);
159 if (logger.isDebugEnabled()) {
160 logger.debug("Received: {}", HexUtils.bytesToHex(response));
162 byte status = response[1], rval = response[2];
164 // No return value to process.
165 } else if (status == 1) {
166 result.parse(new DaliBackwardFrame(rval));
167 } else if (status == 255) {
168 // This is "failure" - daliserver reports this for a garbled response when several ballasts reply. It
169 // should be interpreted as "Yes".
172 throw new DaliException("Invalid response status: " + status);
176 } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
177 | InvocationTargetException e) {