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;
23 import java.util.Collections;
25 import java.util.concurrent.CompletableFuture;
26 import java.util.concurrent.ExecutorService;
27 import java.util.concurrent.Executors;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.openhab.binding.dali.internal.protocol.DaliBackwardFrame;
32 import org.openhab.binding.dali.internal.protocol.DaliCommandBase;
33 import org.openhab.binding.dali.internal.protocol.DaliResponse;
34 import org.openhab.core.common.NamedThreadFactory;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.ThingTypeUID;
40 import org.openhab.core.thing.binding.BaseBridgeHandler;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.util.HexUtils;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * The {@link DaliserverBridgeHandler} handles the lifecycle of daliserver connections.
49 * @author Robert Schmid - Initial contribution
52 public class DaliserverBridgeHandler extends BaseBridgeHandler {
53 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(BRIDGE_TYPE);
55 private final Logger logger = LoggerFactory.getLogger(DaliserverBridgeHandler.class);
56 private static final int DALI_DEFAULT_TIMEOUT = 5000;
58 private DaliserverConfig config = new DaliserverConfig();
59 private @Nullable ExecutorService commandExecutor;
61 public DaliserverBridgeHandler(Bridge thing) {
66 public void handleCommand(ChannelUID channelUID, Command command) {
70 public void initialize() {
71 config = getConfigAs(DaliserverConfig.class);
72 commandExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
73 updateStatus(ThingStatus.ONLINE);
76 private Socket getConnection() throws IOException {
78 logger.debug("Creating connection to daliserver on: {} port: {}", config.host, config.port);
79 Socket socket = new Socket(config.host, config.port);
80 socket.setSoTimeout(DALI_DEFAULT_TIMEOUT);
82 } catch (IOException e) {
83 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
89 public void dispose() {
90 if (commandExecutor != null) {
91 commandExecutor.shutdownNow();
95 public CompletableFuture<@Nullable Void> sendCommand(DaliCommandBase command) {
96 return sendCommandWithResponse(command, DaliResponse.class).thenApply(c -> (Void) null);
99 public <T extends DaliResponse> CompletableFuture<@Nullable T> sendCommandWithResponse(DaliCommandBase command,
100 Class<T> responseType) {
101 CompletableFuture<@Nullable T> future = new CompletableFuture<>();
102 ExecutorService commandExecutor = this.commandExecutor;
103 if (commandExecutor != null) {
104 commandExecutor.submit(() -> {
105 byte[] prefix = new byte[] { 0x2, 0x0 };
106 byte[] message = command.frame.pack();
107 byte[] frame = new byte[prefix.length + message.length];
108 System.arraycopy(prefix, 0, frame, 0, prefix.length);
109 System.arraycopy(message, 0, frame, prefix.length, message.length);
111 try (Socket socket = getConnection();
112 DataOutputStream out = new DataOutputStream(socket.getOutputStream());
113 DataInputStream in = new DataInputStream(socket.getInputStream())) {
115 if (logger.isDebugEnabled()) {
116 logger.debug("Sending: {}", HexUtils.bytesToHex(frame));
119 if (command.sendTwice) {
121 in.readNBytes(4); // discard
129 T response = parseResponse(in, responseType);
130 future.complete(response);
132 } catch (DaliException e) {
133 future.completeExceptionally(e);
136 } catch (SocketTimeoutException e) {
137 logger.warn("Timeout sending command to daliserver: {} Message: {}", frame, e.getMessage());
138 future.completeExceptionally(new DaliException("Timeout sending command to daliserver", e));
139 } catch (IOException e) {
140 logger.warn("Problem sending command to daliserver: {} Message: {}", frame, e.getMessage());
141 future.completeExceptionally(new DaliException("Problem sending command to daliserver", e));
142 } catch (Exception e) {
143 logger.warn("Unexpected exception while sending command to daliserver: {} Message: {}", frame,
145 logger.trace("Stacktrace", e);
146 future.completeExceptionally(e);
150 future.complete(null);
155 private <T extends DaliResponse> @Nullable T parseResponse(DataInputStream reader, Class<T> responseType)
156 throws IOException, DaliException {
158 T result = responseType.getDeclaredConstructor().newInstance();
159 byte[] response = reader.readNBytes(4);
160 if (logger.isDebugEnabled()) {
161 logger.debug("Received: {}", HexUtils.bytesToHex(response));
163 byte status = response[1], rval = response[2];
165 // No return value to process.
166 } else if (status == 1) {
167 result.parse(new DaliBackwardFrame(rval));
168 } else if (status == 255) {
169 // This is "failure" - daliserver reports this for a garbled response when several ballasts reply. It
170 // should be interpreted as "Yes".
173 throw new DaliException("Invalid response status: " + status);
177 } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
178 | InvocationTargetException e) {