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