/** * Copyright (c) 2010-2016 by the respective copyright holders. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.pilight.internal; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Socket; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.lang.StringUtils; import org.codehaus.jackson.JsonGenerationException; import org.codehaus.jackson.JsonGenerator.Feature; import org.codehaus.jackson.map.JsonMappingException; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; import org.openhab.binding.pilight.internal.communication.Action; import org.openhab.binding.pilight.internal.communication.Identification; import org.openhab.binding.pilight.internal.communication.Message; import org.openhab.binding.pilight.internal.communication.Options; import org.openhab.binding.pilight.internal.communication.Response; import org.openhab.binding.pilight.internal.communication.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class listens for updates from the pilight daemon. It is also responsible for requesting * and propagating the current pilight configuration. * * @author Jeroen Idserda * @since 1.0 */ public class PilightConnector extends Thread { private enum ConfigModifyAction { AddListener, ConfigReceived; } private static Logger logger = LoggerFactory.getLogger(PilightConnector.class); private static Integer CONFIG_VALID_TIME = 10000; // 10 seconds private static Integer RECONNECT_DELAY = 10000; // 10 seconds private PilightConnection connection; private IPilightMessageReceivedCallback callback; private ObjectMapper inputMapper = new ObjectMapper() .configure(org.codehaus.jackson.JsonParser.Feature.AUTO_CLOSE_SOURCE, false); private ObjectMapper outputMapper = new ObjectMapper().configure(Feature.AUTO_CLOSE_TARGET, false) .setSerializationInclusion(Inclusion.NON_NULL); private boolean running = true; private boolean updatingConfig = false; private Date lastConfigUpdate; private List<IPilightConfigReceivedCallback> configReceivedCallbacks = new ArrayList<IPilightConfigReceivedCallback>(); private ExecutorService delayedUpdateThreadPool = Executors.newSingleThreadExecutor(); public PilightConnector(PilightConnection connection, IPilightMessageReceivedCallback callback) { this.connection = connection; this.callback = callback; } @Override public void run() { reconnect(); while (running) { try { Socket socket = connection.getSocket(); if (!socket.isClosed()) { BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); String line = in.readLine(); while (running && line != null) { if (!StringUtils.isEmpty(line)) { logger.debug("Received from pilight: {}", line); if (line.startsWith("{\"message\":\"config\"")) { // Configuration received connection.setConfig(inputMapper.readValue(line, Message.class).getConfig()); configAction(ConfigModifyAction.ConfigReceived, null); } else if (line.startsWith("{\"status\":")) { // Status message, we're not using this for now. Response response = inputMapper.readValue(line, Response.class); logger.trace("Response success: " + response.isSuccess()); } else if (line.equals("1")) { // pilight stopping throw new IOException("Connection to pilight lost"); } else { logger.debug(line); Status status = inputMapper.readValue(line, Status.class); callback.messageReceived(connection, status); } } line = in.readLine(); } } } catch (IOException e) { logger.debug("Error in pilight listener thread", e); } logger.info("Disconnected from pilight server at {}:{}", connection.getHostname(), connection.getPort()); if (running) { // empty line received (socket closed) or pilight stopped but binding // is still running, try to reconnect reconnect(); } } } /** * Tells the listener to refetch the configuration * * @param callback {@link IPilightConfigReceivedCallback#configReceived(PilightConnection)} is called when * configuration was received */ public void refreshConfig(IPilightConfigReceivedCallback callback) { try { if (lastConfigUpdate == null || (new Date().getTime() - lastConfigUpdate.getTime()) > CONFIG_VALID_TIME) { configAction(ConfigModifyAction.AddListener, callback); } else { callback.configReceived(connection); } } catch (IOException e) { e.printStackTrace(); } } /** * Stops the listener */ public void close() { running = false; disconnect(); } /** * Determine if this listener is still connected * * @return true when connected */ public boolean isConnected() { return connection.getSocket() != null && !connection.getSocket().isClosed(); } private void notifyConfigReceived() { logger.info("Config for pilight received"); updatingConfig = false; lastConfigUpdate = new Date(); for (IPilightConfigReceivedCallback callback : configReceivedCallbacks) { callback.configReceived(connection); } } private synchronized void configAction(ConfigModifyAction action, IPilightConfigReceivedCallback callback) throws JsonGenerationException, JsonMappingException, IOException { switch (action) { case AddListener: configReceivedCallbacks.add(callback); internalRefreshConfig(); break; case ConfigReceived: notifyConfigReceived(); configReceivedCallbacks.clear(); break; } } private void internalRefreshConfig() throws JsonGenerationException, JsonMappingException, IOException { if (!updatingConfig) { updatingConfig = true; logger.info("Updating pilight config"); Socket socket = connection.getSocket(); outputMapper.writeValue(socket.getOutputStream(), new Action(Action.ACTION_REQUEST_CONFIG)); } } private void disconnect() { if (connection.getSocket() != null) { try { connection.getSocket().close(); } catch (IOException e) { logger.debug("Error while closing pilight socket", e); } } } private void reconnect() { disconnect(); int delay = 0; while (!isConnected()) { try { logger.debug("pilight reconnecting"); Thread.sleep(delay); Socket socket = new Socket(connection.getHostname(), connection.getPort()); Identification identification = new Identification(); Options options = new Options(); options.setConfig(true); identification.setOptions(options); // For some reason, directly using the outputMapper to write to the socket's OutputStream doesn't work. PrintStream printStream = new PrintStream(socket.getOutputStream(), true); printStream.println(outputMapper.writeValueAsString(identification)); Response response = inputMapper.readValue(socket.getInputStream(), Response.class); if (response.getStatus().equals(Response.SUCCESS)) { logger.info("Established connection to pilight server at {}:{}", connection.getHostname(), connection.getPort()); connection.setSocket(socket); } else { logger.debug("pilight client not accepted: {}", response.getStatus()); } } catch (IOException e) { logger.debug(e.getMessage(), e); } catch (InterruptedException e) { logger.debug(e.getMessage(), e); } delay = RECONNECT_DELAY; } } public void doUpdate(Action action) { if (isConnected()) { if (connection.getDelay() != null) { delayedUpdateCall(action); } else { doUpdateCall(action); } } else { logger.debug("Cannot send command, not connected to pilight"); } } private void delayedUpdateCall(Action action) { DelayedUpdate delayed = new DelayedUpdate(action, connection); delayedUpdateThreadPool.execute(delayed); } private void doUpdateCall(Action action) { try { connection.setLastUpdate(new Date()); outputMapper.writeValue(connection.getSocket().getOutputStream(), action); } catch (IOException e) { logger.debug("Error while sending update to pilight server", e); } } /** * Simple thread to allow calls to pilight to be throttled * * @author Jeroen Idserda * @since 1.0 */ private class DelayedUpdate implements Runnable { private Action action; private PilightConnection connection; public DelayedUpdate(Action action, PilightConnection connection) { this.action = action; this.connection = connection; } @Override public void run() { long delayBetweenUpdates = connection.getDelay(); if (connection.getLastUpdate() != null) { long diff = new Date().getTime() - connection.getLastUpdate().getTime(); if (diff < delayBetweenUpdates) { long delay = delayBetweenUpdates - diff; try { Thread.sleep(delay); } catch (InterruptedException e) { logger.debug("Error while processing pilight throttling delay"); } } } doUpdateCall(action); } } }