]> git.basschouten.com Git - openhab-addons.git/blob
e05928c2434a63988decb17aac149709211cad44
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.nikobus.internal.handler;
14
15 import static org.openhab.binding.nikobus.internal.NikobusBindingConstants.CONFIG_REFRESH_INTERVAL;
16
17 import java.io.IOException;
18 import java.io.OutputStream;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.LinkedList;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27 import java.util.stream.Collectors;
28 import java.util.stream.Stream;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.nikobus.internal.NikobusBindingConstants;
33 import org.openhab.binding.nikobus.internal.protocol.NikobusCommand;
34 import org.openhab.binding.nikobus.internal.protocol.NikobusConnection;
35 import org.openhab.binding.nikobus.internal.utils.Utils;
36 import org.openhab.core.io.transport.serial.SerialPortManager;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseBridgeHandler;
43 import org.openhab.core.thing.binding.ThingHandler;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 /**
49  * The {@link NikobusPcLinkHandler} is responsible for handling commands, which are
50  * sent or received from the PC-Link Nikobus component.
51  *
52  * @author Boris Krivonog - Initial contribution
53  */
54 @NonNullByDefault
55 public class NikobusPcLinkHandler extends BaseBridgeHandler {
56     private final Logger logger = LoggerFactory.getLogger(NikobusPcLinkHandler.class);
57     private final Map<String, Runnable> commandListeners = Collections.synchronizedMap(new HashMap<>());
58     private final LinkedList<NikobusCommand> pendingCommands = new LinkedList<>();
59     private final StringBuilder stringBuilder = new StringBuilder();
60     private final SerialPortManager serialPortManager;
61     private @Nullable NikobusConnection connection;
62     private @Nullable NikobusCommand currentCommand;
63     private @Nullable ScheduledFuture<?> scheduledRefreshFuture;
64     private @Nullable ScheduledFuture<?> scheduledSendCommandWatchdogFuture;
65     private @Nullable String ack;
66     private int refreshThingIndex = 0;
67
68     public NikobusPcLinkHandler(Bridge bridge, SerialPortManager serialPortManager) {
69         super(bridge);
70         this.serialPortManager = serialPortManager;
71     }
72
73     @Override
74     public void initialize() {
75         ack = null;
76         stringBuilder.setLength(0);
77
78         updateStatus(ThingStatus.UNKNOWN);
79
80         String portName = (String) getConfig().get(NikobusBindingConstants.CONFIG_PORT_NAME);
81         if (portName == null) {
82             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Port must be set!");
83             return;
84         }
85
86         connection = new NikobusConnection(serialPortManager, portName, this::processReceivedValue);
87
88         int refreshInterval = ((Number) getConfig().get(CONFIG_REFRESH_INTERVAL)).intValue();
89         scheduledRefreshFuture = scheduler.scheduleWithFixedDelay(this::refresh, refreshInterval, refreshInterval,
90                 TimeUnit.SECONDS);
91     }
92
93     @Override
94     public void dispose() {
95         super.dispose();
96
97         Utils.cancel(scheduledSendCommandWatchdogFuture);
98         scheduledSendCommandWatchdogFuture = null;
99
100         Utils.cancel(scheduledRefreshFuture);
101         scheduledRefreshFuture = null;
102
103         NikobusConnection connection = this.connection;
104         this.connection = null;
105
106         if (connection != null) {
107             connection.close();
108         }
109     }
110
111     @Override
112     public void handleCommand(ChannelUID channelUID, Command command) {
113         // Noop.
114     }
115
116     @SuppressWarnings("null")
117     private void processReceivedValue(byte value) {
118         logger.trace("Received {}", value);
119
120         if (value == 13) {
121             String command = stringBuilder.toString();
122             stringBuilder.setLength(0);
123
124             logger.debug("Received command '{}', ack = '{}'", command, ack);
125
126             try {
127                 if (command.startsWith("$")) {
128                     String ack = this.ack;
129                     this.ack = null;
130
131                     processResponse(command, ack);
132                 } else {
133                     Runnable listener = commandListeners.get(command);
134                     if (listener != null) {
135                         listener.run();
136                     }
137                 }
138             } catch (RuntimeException e) {
139                 logger.debug("Processing command '{}' failed due {}", command, e.getMessage(), e);
140             }
141         } else {
142             stringBuilder.append((char) value);
143
144             // Take ACK part, i.e. "$0512"
145             if (stringBuilder.length() == 5) {
146                 String payload = stringBuilder.toString();
147                 if (payload.startsWith("$05")) {
148                     ack = payload;
149                     logger.debug("Received ack '{}'", ack);
150                     stringBuilder.setLength(0);
151                 }
152             } else if (stringBuilder.length() > 128) {
153                 // Fuse, if for some reason we don't receive \r don't fill buffer.
154                 stringBuilder.setLength(0);
155                 logger.warn("Resetting read buffer, should not happen, am I connected to Nikobus?");
156             }
157         }
158     }
159
160     @SuppressWarnings("null")
161     public void addListener(String command, Runnable listener) {
162         if (commandListeners.put(command, listener) != null) {
163             logger.warn("Multiple registrations for '{}'", command);
164         }
165     }
166
167     public void removeListener(String command) {
168         commandListeners.remove(command);
169     }
170
171     private void processResponse(String commandPayload, @Nullable String ack) {
172         NikobusCommand command;
173         synchronized (pendingCommands) {
174             command = currentCommand;
175         }
176
177         if (command == null) {
178             logger.debug("Processing response but no command pending");
179             return;
180         }
181
182         NikobusCommand.ResponseHandler responseHandler = command.getResponseHandler();
183         if (responseHandler == null) {
184             logger.debug("No response expected for current command");
185             return;
186         }
187
188         if (ack == null) {
189             logger.debug("No ack received");
190             return;
191         }
192
193         String requestCommandId = command.getPayload().substring(3, 5);
194         String ackCommandId = ack.substring(3, 5);
195         if (!ackCommandId.equals(requestCommandId)) {
196             logger.debug("Unexpected command's ack '{}' != '{}'", requestCommandId, ackCommandId);
197             return;
198         }
199
200         // Check if response has expected length.
201         if (commandPayload.length() != responseHandler.getResponseLength()) {
202             logger.debug("Unexpected response length");
203             return;
204         }
205
206         if (!commandPayload.startsWith(responseHandler.getResponseCode())) {
207             logger.debug("Unexpected response command code");
208             return;
209         }
210
211         String requestCommandAddress = command.getPayload().substring(5, 9);
212         String ackCommandAddress = commandPayload.substring(responseHandler.getAddressStart(),
213                 responseHandler.getAddressStart() + 4);
214         if (!requestCommandAddress.equals(ackCommandAddress)) {
215             logger.debug("Unexpected response address");
216             return;
217         }
218
219         if (responseHandler.complete(commandPayload)) {
220             resetProcessingAndProcessNext();
221         }
222     }
223
224     public void sendCommand(NikobusCommand command) {
225         synchronized (pendingCommands) {
226             pendingCommands.addLast(command);
227         }
228
229         scheduler.submit(this::processCommand);
230     }
231
232     @SuppressWarnings({ "unused", "null" })
233     private void processCommand() {
234         NikobusCommand command;
235         synchronized (pendingCommands) {
236             if (currentCommand != null) {
237                 return;
238             }
239
240             command = pendingCommands.pollFirst();
241             if (command == null) {
242                 return;
243             }
244
245             currentCommand = command;
246         }
247         sendCommand(command, 3);
248     }
249
250     private void sendCommand(NikobusCommand command, int retry) {
251         logger.debug("Sending retry = {}, command '{}'", retry, command.getPayload());
252
253         NikobusConnection connection = this.connection;
254         if (connection == null) {
255             return;
256         }
257
258         try {
259             connectIfNeeded(connection);
260
261             OutputStream outputStream = connection.getOutputStream();
262             if (outputStream == null) {
263                 return;
264             }
265             outputStream.write(command.getPayload().getBytes());
266             outputStream.flush();
267         } catch (IOException e) {
268             logger.debug("Sending command failed due {}", e.getMessage(), e);
269             connection.close();
270             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
271         } finally {
272             NikobusCommand.ResponseHandler responseHandler = command.getResponseHandler();
273             if (responseHandler == null) {
274                 resetProcessingAndProcessNext();
275             } else if (retry > 0) {
276                 scheduleSendCommandTimeout(() -> {
277                     if (!responseHandler.isCompleted()) {
278                         sendCommand(command, retry - 1);
279                     }
280                 });
281             } else {
282                 scheduleSendCommandTimeout(() -> processTimeout(responseHandler));
283             }
284         }
285     }
286
287     private void scheduleSendCommandTimeout(Runnable command) {
288         scheduledSendCommandWatchdogFuture = scheduler.schedule(command, 2, TimeUnit.SECONDS);
289     }
290
291     private void processTimeout(NikobusCommand.ResponseHandler responseHandler) {
292         if (responseHandler.completeExceptionally(new TimeoutException("Waiting for response timed-out."))) {
293             resetProcessingAndProcessNext();
294         }
295     }
296
297     private void resetProcessingAndProcessNext() {
298         Utils.cancel(scheduledSendCommandWatchdogFuture);
299         synchronized (pendingCommands) {
300             currentCommand = null;
301         }
302         scheduler.submit(this::processCommand);
303     }
304
305     private void refresh() {
306         List<Thing> things = getThing().getThings().stream()
307                 .filter(thing -> thing.getHandler() instanceof NikobusModuleHandler).collect(Collectors.toList());
308
309         if (things.isEmpty()) {
310             logger.debug("Nothing to refresh");
311             return;
312         }
313
314         refreshThingIndex = (refreshThingIndex + 1) % things.size();
315
316         ThingHandler thingHandler = things.get(refreshThingIndex).getHandler();
317         if (thingHandler == null) {
318             return;
319         }
320
321         NikobusModuleHandler handler = (NikobusModuleHandler) thingHandler;
322         handler.refreshModule();
323     }
324
325     private synchronized void connectIfNeeded(NikobusConnection connection) throws IOException {
326         if (!connection.isConnected()) {
327             connection.connect();
328
329             // Send connection sequence, mimicking the Nikobus software. If this is not send, PC-Link
330             // sometimes does not forward button presses via serial interface.
331             Stream.of(new String[] { "++++", "ATH0", "ATZ", "$10110000B8CF9D", "#L0", "#E0", "#L0", "#E1" })
332                     .map(NikobusCommand::new).forEach(this::sendCommand);
333
334             updateStatus(ThingStatus.ONLINE);
335         }
336     }
337 }