2 * Copyright (c) 2010-2021 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.pilight.internal;
16 import java.net.Socket;
17 import java.util.Collections;
18 import java.util.concurrent.*;
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;
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;
34 * This class listens for updates from the pilight daemon. It is also responsible for requesting
35 * and propagating the current pilight configuration.
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
43 public class PilightConnector implements Runnable, Closeable {
45 private static final int RECONNECT_DELAY_MSEC = 10 * 1000; // 10 seconds
47 private final Logger logger = LoggerFactory.getLogger(PilightConnector.class);
49 private final PilightBridgeConfiguration config;
51 private final IPilightCallback callback;
53 private final ObjectMapper inputMapper = new ObjectMapper(
54 new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false));
56 private final ObjectMapper outputMapper = new ObjectMapper(
57 new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false))
58 .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
60 private @Nullable Socket socket;
61 private @Nullable PrintStream printStream;
63 private final ScheduledExecutorService scheduler;
64 private final ConcurrentLinkedQueue<Action> delayedActionQueue = new ConcurrentLinkedQueue<>();
65 private @Nullable ScheduledFuture<?> delayedActionWorkerFuture;
67 public PilightConnector(final PilightBridgeConfiguration config, final IPilightCallback callback,
68 final ScheduledExecutorService scheduler) {
70 this.callback = callback;
71 this.scheduler = scheduler;
79 while (!Thread.currentThread().isInterrupted()) {
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\":")) {
100 } else if (line.equals("1")) {
101 throw new IOException("Connection to pilight lost");
103 final @Nullable Status status = inputMapper.readValue(line, Status.class);
104 callback.statusReceived(Collections.singletonList(status));
108 line = in.readLine();
112 } catch (IOException e) {
113 if (!Thread.currentThread().isInterrupted()) {
114 logger.debug("Error in pilight listener thread: {}", e.getMessage());
118 logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
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
128 } catch (InterruptedException e) {
129 logger.debug("Interrupting thread.");
130 Thread.currentThread().interrupt();
135 * Tells the connector to refresh the configuration
137 public void refreshConfig() {
138 doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
142 * Tells the connector to refresh the status of all devices
144 public void refreshStatus() {
145 doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
151 public void close() {
153 Thread.currentThread().interrupt();
156 private void disconnect() {
157 final @Nullable PrintStream printStream = this.printStream;
158 if (printStream != null) {
160 this.printStream = null;
163 final @Nullable Socket socket = this.socket;
164 if (socket != null) {
167 } catch (IOException e) {
168 logger.debug("Error while closing pilight socket: {}", e.getMessage());
174 private boolean isConnected() {
175 final @Nullable Socket socket = this.socket;
176 return socket != null && !socket.isClosed();
179 private void connect() throws InterruptedException {
184 while (!isConnected()) {
186 logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort());
189 Socket socket = new Socket(config.getIpAddress(), config.getPort());
191 Options options = new Options();
192 options.setConfig(true);
194 Identification identification = new Identification();
195 identification.setOptions(options);
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));
201 final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
203 if (response.getStatus().equals(Response.SUCCESS)) {
204 logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(),
206 this.socket = socket;
207 this.printStream = printStream;
208 callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
212 logger.debug("pilight client not accepted: {}", response.getStatus());
214 } catch (IOException e) {
215 final @Nullable PrintStream printStream = this.printStream;
216 if (printStream != null) {
219 logger.debug("connect failed: {}", e.getMessage());
220 callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
223 delay = RECONNECT_DELAY_MSEC;
228 * send action to pilight daemon
230 * @param action action to send
232 public void sendAction(Action action) {
233 delayedActionQueue.add(action);
234 final @Nullable ScheduledFuture<?> delayedActionWorkerFuture = this.delayedActionWorkerFuture;
236 if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) {
237 this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> {
238 if (!delayedActionQueue.isEmpty()) {
239 doSendAction(delayedActionQueue.poll());
241 final @Nullable ScheduledFuture<?> workerFuture = this.delayedActionWorkerFuture;
242 if (workerFuture != null) {
243 workerFuture.cancel(false);
245 this.delayedActionWorkerFuture = null;
247 }, 0, config.getDelay(), TimeUnit.MILLISECONDS);
251 private void doSendAction(Action action) {
252 final @Nullable PrintStream printStream = this.printStream;
253 if (printStream != null) {
255 printStream.println(outputMapper.writeValueAsString(action));
256 } catch (IOException e) {
257 logger.debug("Error while sending action '{}' to pilight server: {}", action.getAction(),
261 logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());