]> git.basschouten.com Git - openhab-addons.git/blob
75fca4f9cf4e20d3ba7ebd8080bec6dc206c45d5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.pilight.internal;
14
15 import java.io.*;
16 import java.net.Socket;
17 import java.util.Collections;
18 import java.util.concurrent.*;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.pilight.internal.dto.*;
23 import org.openhab.core.thing.ThingStatus;
24 import org.openhab.core.thing.ThingStatusDetail;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27
28 import com.fasterxml.jackson.annotation.JsonInclude;
29 import com.fasterxml.jackson.core.JsonParser;
30 import com.fasterxml.jackson.databind.MappingJsonFactory;
31 import com.fasterxml.jackson.databind.ObjectMapper;
32
33 /**
34  * This class listens for updates from the pilight daemon. It is also responsible for requesting
35  * and propagating the current pilight configuration.
36  *
37  * @author Jeroen Idserda - Initial contribution
38  * @author Stefan Röllin - Port to openHAB 2 pilight binding
39  * @author Niklas Dörfler - Port pilight binding to openHAB 3 + add device discovery
40  *
41  */
42 @NonNullByDefault
43 public class PilightConnector implements Runnable, Closeable {
44
45     private static final int RECONNECT_DELAY_MSEC = 10 * 1000; // 10 seconds
46
47     private final Logger logger = LoggerFactory.getLogger(PilightConnector.class);
48
49     private final PilightBridgeConfiguration config;
50
51     private final IPilightCallback callback;
52
53     private final ObjectMapper inputMapper = new ObjectMapper(
54             new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false));
55
56     private final ObjectMapper outputMapper = new ObjectMapper(
57             new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false))
58                     .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
59
60     private @Nullable Socket socket;
61     private @Nullable PrintStream printStream;
62
63     private final ScheduledExecutorService scheduler;
64     private final ConcurrentLinkedQueue<Action> delayedActionQueue = new ConcurrentLinkedQueue<>();
65     private @Nullable ScheduledFuture<?> delayedActionWorkerFuture;
66
67     public PilightConnector(final PilightBridgeConfiguration config, final IPilightCallback callback,
68             final ScheduledExecutorService scheduler) {
69         this.config = config;
70         this.callback = callback;
71         this.scheduler = scheduler;
72     }
73
74     @Override
75     public void run() {
76         try {
77             connect();
78
79             while (!Thread.currentThread().isInterrupted()) {
80                 try {
81                     final @Nullable Socket socket = this.socket;
82                     if (socket != null && !socket.isClosed()) {
83                         try (BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
84                             String line = in.readLine();
85                             while (!Thread.currentThread().isInterrupted() && line != null) {
86                                 if (!line.isEmpty()) {
87                                     logger.trace("Received from pilight: {}", line);
88                                     final ObjectMapper inputMapper = this.inputMapper;
89                                     if (line.startsWith("{\"message\":\"config\"")) {
90                                         final @Nullable Message message = inputMapper.readValue(line, Message.class);
91                                         callback.configReceived(message.getConfig());
92                                     } else if (line.startsWith("{\"message\":\"values\"")) {
93                                         final @Nullable AllStatus status = inputMapper.readValue(line, AllStatus.class);
94                                         callback.statusReceived(status.getValues());
95                                     } else if (line.startsWith("{\"version\":")) {
96                                         final @Nullable Version version = inputMapper.readValue(line, Version.class);
97                                         callback.versionReceived(version);
98                                     } else if (line.startsWith("{\"status\":")) {
99                                         // currently unused
100                                     } else if (line.equals("1")) {
101                                         throw new IOException("Connection to pilight lost");
102                                     } else {
103                                         final @Nullable Status status = inputMapper.readValue(line, Status.class);
104                                         callback.statusReceived(Collections.singletonList(status));
105                                     }
106                                 }
107
108                                 line = in.readLine();
109                             }
110                         }
111                     }
112                 } catch (IOException e) {
113                     if (!Thread.currentThread().isInterrupted()) {
114                         logger.debug("Error in pilight listener thread: {}", e.getMessage());
115                     }
116                 }
117
118                 logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
119
120                 if (!Thread.currentThread().isInterrupted()) {
121                     callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, null);
122                     // empty line received (socket closed) or pilight stopped but binding
123                     // is still running, try to reconnect
124                     connect();
125                 }
126             }
127
128         } catch (InterruptedException e) {
129             logger.debug("Interrupting thread.");
130             Thread.currentThread().interrupt();
131         }
132     }
133
134     /**
135      * Tells the connector to refresh the configuration
136      */
137     public void refreshConfig() {
138         doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
139     }
140
141     /**
142      * Tells the connector to refresh the status of all devices
143      */
144     public void refreshStatus() {
145         doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
146     }
147
148     /**
149      * Stops the listener
150      */
151     public void close() {
152         disconnect();
153         Thread.currentThread().interrupt();
154     }
155
156     private void disconnect() {
157         final @Nullable PrintStream printStream = this.printStream;
158         if (printStream != null) {
159             printStream.close();
160             this.printStream = null;
161         }
162
163         final @Nullable Socket socket = this.socket;
164         if (socket != null) {
165             try {
166                 socket.close();
167             } catch (IOException e) {
168                 logger.debug("Error while closing pilight socket: {}", e.getMessage());
169             }
170             this.socket = null;
171         }
172     }
173
174     private boolean isConnected() {
175         final @Nullable Socket socket = this.socket;
176         return socket != null && !socket.isClosed();
177     }
178
179     private void connect() throws InterruptedException {
180         disconnect();
181
182         int delay = 0;
183
184         while (!isConnected()) {
185             try {
186                 logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort());
187
188                 Thread.sleep(delay);
189                 Socket socket = new Socket(config.getIpAddress(), config.getPort());
190
191                 Options options = new Options();
192                 options.setConfig(true);
193
194                 Identification identification = new Identification();
195                 identification.setOptions(options);
196
197                 // For some reason, directly using the outputMapper to write to the socket's OutputStream doesn't work.
198                 PrintStream printStream = new PrintStream(socket.getOutputStream(), true);
199                 printStream.println(outputMapper.writeValueAsString(identification));
200
201                 final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
202
203                 if (response.getStatus().equals(Response.SUCCESS)) {
204                     logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(),
205                             config.getPort());
206                     this.socket = socket;
207                     this.printStream = printStream;
208                     callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
209                 } else {
210                     printStream.close();
211                     socket.close();
212                     logger.debug("pilight client not accepted: {}", response.getStatus());
213                 }
214             } catch (IOException e) {
215                 final @Nullable PrintStream printStream = this.printStream;
216                 if (printStream != null) {
217                     printStream.close();
218                 }
219                 logger.debug("connect failed: {}", e.getMessage());
220                 callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
221             }
222
223             delay = RECONNECT_DELAY_MSEC;
224         }
225     }
226
227     /**
228      * send action to pilight daemon
229      *
230      * @param action action to send
231      */
232     public void sendAction(Action action) {
233         delayedActionQueue.add(action);
234         final @Nullable ScheduledFuture<?> delayedActionWorkerFuture = this.delayedActionWorkerFuture;
235
236         if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) {
237             this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> {
238                 if (!delayedActionQueue.isEmpty()) {
239                     doSendAction(delayedActionQueue.poll());
240                 } else {
241                     final @Nullable ScheduledFuture<?> workerFuture = this.delayedActionWorkerFuture;
242                     if (workerFuture != null) {
243                         workerFuture.cancel(false);
244                     }
245                     this.delayedActionWorkerFuture = null;
246                 }
247             }, 0, config.getDelay(), TimeUnit.MILLISECONDS);
248         }
249     }
250
251     private void doSendAction(Action action) {
252         final @Nullable PrintStream printStream = this.printStream;
253         if (printStream != null) {
254             try {
255                 printStream.println(outputMapper.writeValueAsString(action));
256             } catch (IOException e) {
257                 logger.debug("Error while sending action '{}' to pilight server: {}", action.getAction(),
258                         e.getMessage());
259             }
260         } else {
261             logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());
262         }
263     }
264 }