2 * Copyright (c) 2010-2024 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;
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;
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;
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;
48 * This class listens for updates from the pilight daemon. It is also responsible for requesting
49 * and propagating the current pilight configuration.
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
57 public class PilightConnector implements Runnable, Closeable {
59 private static final int RECONNECT_DELAY_MSEC = 10 * 1000; // 10 seconds
61 private final Logger logger = LoggerFactory.getLogger(PilightConnector.class);
63 private final PilightBridgeConfiguration config;
65 private final IPilightCallback callback;
67 private final ObjectMapper inputMapper = new ObjectMapper(
68 new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false));
70 private final ObjectMapper outputMapper = new ObjectMapper(
71 new MappingJsonFactory().configure(JsonParser.Feature.AUTO_CLOSE_SOURCE, false))
72 .setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
74 private @Nullable Socket socket;
75 private @Nullable PrintStream printStream;
77 private final ScheduledExecutorService scheduler;
78 private final ConcurrentLinkedQueue<Action> delayedActionQueue = new ConcurrentLinkedQueue<>();
79 private @Nullable ScheduledFuture<?> delayedActionWorkerFuture;
81 public PilightConnector(final PilightBridgeConfiguration config, final IPilightCallback callback,
82 final ScheduledExecutorService scheduler) {
84 this.callback = callback;
85 this.scheduler = scheduler;
93 while (!Thread.currentThread().isInterrupted()) {
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");
116 final @Nullable Status status = inputMapper.readValue(line, Status.class);
117 callback.statusReceived(List.of(status));
121 line = in.readLine();
125 } catch (IOException e) {
126 if (!Thread.currentThread().isInterrupted()) {
127 logger.debug("Error in pilight listener thread: {}", e.getMessage());
131 logger.debug("Disconnected from pilight server at {}:{}", config.getIpAddress(), config.getPort());
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
141 } catch (InterruptedException e) {
142 logger.debug("Interrupting thread.");
143 Thread.currentThread().interrupt();
148 * Tells the connector to refresh the configuration
150 public void refreshConfig() {
151 doSendAction(new Action(Action.ACTION_REQUEST_CONFIG));
155 * Tells the connector to refresh the status of all devices
157 public void refreshStatus() {
158 doSendAction(new Action(Action.ACTION_REQUEST_VALUES));
165 public void close() {
167 Thread.currentThread().interrupt();
170 private void disconnect() {
171 final @Nullable PrintStream printStream = this.printStream;
172 if (printStream != null) {
174 this.printStream = null;
177 final @Nullable Socket socket = this.socket;
178 if (socket != null) {
181 } catch (IOException e) {
182 logger.debug("Error while closing pilight socket: {}", e.getMessage());
188 private boolean isConnected() {
189 final @Nullable Socket socket = this.socket;
190 return socket != null && !socket.isClosed();
193 private void connect() throws InterruptedException {
198 while (!isConnected()) {
200 logger.debug("pilight connecting to {}:{}", config.getIpAddress(), config.getPort());
203 Socket socket = new Socket(config.getIpAddress(), config.getPort());
205 Options options = new Options();
206 options.setConfig(true);
208 Identification identification = new Identification();
209 identification.setOptions(options);
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));
215 final @Nullable Response response = inputMapper.readValue(socket.getInputStream(), Response.class);
217 if (response.getStatus().equals(Response.SUCCESS)) {
218 logger.debug("Established connection to pilight server at {}:{}", config.getIpAddress(),
220 this.socket = socket;
221 this.printStream = printStream;
222 callback.updateThingStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
226 logger.debug("pilight client not accepted: {}", response.getStatus());
228 } catch (IOException e) {
229 final @Nullable PrintStream printStream = this.printStream;
230 if (printStream != null) {
233 logger.debug("connect failed: {}", e.getMessage());
234 callback.updateThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
237 delay = RECONNECT_DELAY_MSEC;
242 * send action to pilight daemon
244 * @param action action to send
246 public void sendAction(Action action) {
247 delayedActionQueue.add(action);
248 final @Nullable ScheduledFuture<?> delayedActionWorkerFuture = this.delayedActionWorkerFuture;
250 if (delayedActionWorkerFuture == null || delayedActionWorkerFuture.isCancelled()) {
251 this.delayedActionWorkerFuture = scheduler.scheduleWithFixedDelay(() -> {
252 if (!delayedActionQueue.isEmpty()) {
253 doSendAction(delayedActionQueue.poll());
255 final @Nullable ScheduledFuture<?> workerFuture = this.delayedActionWorkerFuture;
256 if (workerFuture != null) {
257 workerFuture.cancel(false);
259 this.delayedActionWorkerFuture = null;
261 }, 0, config.getDelay(), TimeUnit.MILLISECONDS);
265 private void doSendAction(Action action) {
266 final @Nullable PrintStream printStream = this.printStream;
267 if (printStream != null) {
269 printStream.println(outputMapper.writeValueAsString(action));
270 } catch (IOException e) {
271 logger.debug("Error while sending action '{}' to pilight server: {}", action.getAction(),
275 logger.debug("Cannot send action '{}', not connected to pilight!", action.getAction());