]> git.basschouten.com Git - openhab-addons.git/blob
5ffc6405665037c5fa3dfd614313a80deaad8c54
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.List;
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                                 logger.trace("Received from pilight: {}", line);
101                                 // ignore empty lines and lines starting with "status"
102                                 if (!line.isEmpty() && !line.startsWith("{\"status\":")) {
103                                     final ObjectMapper inputMapper = this.inputMapper;
104                                     if (line.startsWith("{\"message\":\"config\"")) {
105                                         final @Nullable Message message = inputMapper.readValue(line, Message.class);
106                                         callback.configReceived(message.getConfig());
107                                     } else if (line.startsWith("{\"message\":\"values\"")) {
108                                         final @Nullable AllStatus status = inputMapper.readValue(line, AllStatus.class);
109                                         callback.statusReceived(status.getValues());
110                                     } else if (line.startsWith("{\"version\":")) {
111                                         final @Nullable Version version = inputMapper.readValue(line, Version.class);
112                                         callback.versionReceived(version);
113                                     } else if ("1".equals(line)) {
114                                         throw new IOException("Connection to pilight lost");
115                                     } else {
116                                         final @Nullable Status status = inputMapper.readValue(line, Status.class);
117                                         callback.statusReceived(List.of(status));
118                                     }
119                                 }
120
121                                 line = in.readLine();
122                             }
123                         }
124                     }
125                 } catch (IOException e) {
126                     if (!Thread.currentThread().isInterrupted()) {
127                         logger.debug("Error in pilight listener thread: {}", e.getMessage());
128                     }
129                 }
130
131                 logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
132
133                 if (!Thread.currentThread().isInterrupted()) {
134                     callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, null);
135                     // empty line received (socket closed) or pilight stopped but binding
136                     // is still running, try to reconnect
137                     connect();
138                 }
139             }
140
141         } catch (InterruptedException e) {
142             logger.debug("Interrupting thread.");
143             Thread.currentThread().interrupt();
144         }
145     }
146
147     /**
148      * Tells the connector to refresh the configuration
149      */
150     public void refreshConfig() {
151         doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
152     }
153
154     /**
155      * Tells the connector to refresh the status of all devices
156      */
157     public void refreshStatus() {
158         doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
159     }
160
161     /**
162      * Stops the listener
163      */
164     @Override
165     public void close() {
166         disconnect();
167         Thread.currentThread().interrupt();
168     }
169
170     private void disconnect() {
171         final @Nullable PrintStream printStream = this.printStream;
172         if (printStream != null) {
173             printStream.close();
174             this.printStream = null;
175         }
176
177         final @Nullable Socket socket = this.socket;
178         if (socket != null) {
179             try {
180                 socket.close();
181             } catch (IOException e) {
182                 logger.debug("Error while closing pilight socket: {}", e.getMessage());
183             }
184             this.socket = null;
185         }
186     }
187
188     private boolean isConnected() {
189         final @Nullable Socket socket = this.socket;
190         return socket != null && !socket.isClosed();
191     }
192
193     private void connect() throws InterruptedException {
194         disconnect();
195
196         int delay = 0;
197
198         while (!isConnected()) {
199             try {
200                 logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort());
201
202                 Thread.sleep(delay);
203                 Socket socket = new Socket(config.getIpAddress(), config.getPort());
204
205                 Options options = new Options();
206                 options.setConfig(true);
207
208                 Identification identification = new Identification();
209                 identification.setOptions(options);
210
211                 // For some reason, directly using the outputMapper to write to the socket's OutputStream doesn't work.
212                 PrintStream printStream = new PrintStream(socket.getOutputStream(), true);
213                 printStream.println(outputMapper.writeValueAsString(identification));
214
215                 final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
216
217                 if (response.getStatus().equals(Response.SUCCESS)) {
218                     logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(),
219                             config.getPort());
220                     this.socket = socket;
221                     this.printStream = printStream;
222                     callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
223                 } else {
224                     printStream.close();
225                     socket.close();
226                     logger.debug("pilight client not accepted: {}", response.getStatus());
227                 }
228             } catch (IOException e) {
229                 final @Nullable PrintStream printStream = this.printStream;
230                 if (printStream != null) {
231                     printStream.close();
232                 }
233                 logger.debug("connect failed: {}", e.getMessage());
234                 callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
235             }
236
237             delay = RECONNECT_DELAY_MSEC;
238         }
239     }
240
241     /**
242      * send action to pilight daemon
243      *
244      * @param action action to send
245      */
246     public void sendAction(Action action) {
247         delayedActionQueue.add(action);
248         final @Nullable ScheduledFuture<?> delayedActionWorkerFuture = this.delayedActionWorkerFuture;
249
250         if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) {
251             this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> {
252                 if (!delayedActionQueue.isEmpty()) {
253                     doSendAction(delayedActionQueue.poll());
254                 } else {
255                     final @Nullable ScheduledFuture<?> workerFuture = this.delayedActionWorkerFuture;
256                     if (workerFuture != null) {
257                         workerFuture.cancel(false);
258                     }
259                     this.delayedActionWorkerFuture = null;
260                 }
261             }, 0, config.getDelay(), TimeUnit.MILLISECONDS);
262         }
263     }
264
265     private void doSendAction(Action action) {
266         final @Nullable PrintStream printStream = this.printStream;
267         if (printStream != null) {
268             try {
269                 printStream.println(outputMapper.writeValueAsString(action));
270             } catch (IOException e) {
271                 logger.debug("Error while sending action '{}' to pilight server: {}", action.getAction(),
272                         e.getMessage());
273             }
274         } else {
275             logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());
276         }
277     }
278 }