/** * Wrapper to turn a Reader into an InputStream. The previous version of this class did not handle * correctly encode Unicode characters; it was used primarily for hash filtering, where the result * must be a reproducible byte sequence but needn't necessarily be exactly reconstructible to the * original character sequence. This stream is now used by link rewriters, which do have to produce * a valid decodable result, so it now handles character encoding. But this version isn't * necessarily reproducible because some encodings aren't unique - the exact sequence of bytes * produced can depend on the number of bytes read at a time. * * <p>If called with no encoder, this version reproduces the previous behavior, making it * polling-compatible. */ public class ReaderInputStream extends InputStream { protected static Logger log = Logger.getLogger("ReaderInputStream"); /** Source Reader */ private Reader in; private String encoding = null; private Charset cset; private CharsetEncoder encoder; private byte[] slack; private int begin; /** * Construct a <code>ReaderInputStream</code> for the specified <code>Reader</code>. This version * (no encoding specified) converts characters to bytes by casting, to be consistent with previous * behavior. * * @param reader <code>Reader</code>. Must not be <code>null</code>. */ public ReaderInputStream(Reader reader) { if (reader == null) { throw new IllegalArgumentException("reader must not be null"); } in = reader; } /** * Construct a <code>ReaderInputStream</code> for the specified <code>Reader</code>, with the * specified encoding. * * @param reader non-null <code>Reader</code>. * @param encoding charset name */ public ReaderInputStream(Reader reader, String encoding) { this(reader); this.encoding = encoding; } /** Returns the next byte of the encoded character sequence */ public synchronized int read() throws IOException { if (in == null) { throw new IOException("Stream Closed"); } byte result; if (slack != null && begin < slack.length) { result = slack[begin]; if (++begin == slack.length) { slack = null; } } else { byte[] buf = new byte[1]; if (read(buf, 0, 1) <= 0) { return -1; } else { result = buf[0]; } } return result & 0xFF; } /** * Reads from the <code>Reader</code> into a byte array * * @param b the byte array to read into * @param off the offset in the byte array * @param len the length in the byte array to fill * @return the actual number read into the byte array, -1 at the end of the stream * @exception IOException if an error occurs */ public synchronized int read(byte[] b, int off, int len) throws IOException { if (in == null) { throw new IOException("Stream Closed"); } if (len == 0) { return 0; } if (encoding == null) { return castingRead(b, off, len); } while (slack == null) { char[] buf = new char[len]; // might read too much int n = in.read(buf); if (n == -1) { return -1; } if (n > 0) { slack = new String(buf, 0, n).getBytes(encoding); begin = 0; } } if (len > slack.length - begin) { len = slack.length - begin; } System.arraycopy(slack, begin, b, off, len); if ((begin += len) >= slack.length) { slack = null; } return len; } static final int DEFAULT_BUFFER_CAPACITY = 16384; private char[] charBuffer; // Old version simply casts. Must still be used for hashing unless // change polling version private int castingRead(byte[] outputBuf, int off, int len) throws IOException { if (charBuffer == null) { charBuffer = new char[DEFAULT_BUFFER_CAPACITY]; } if (len > DEFAULT_BUFFER_CAPACITY) { len = DEFAULT_BUFFER_CAPACITY; } int numRead = in.read(charBuffer, 0, len); for (int ix = 0; ix < numRead; ix++) { outputBuf[off + ix] = (byte) charBuffer[ix]; } return numRead; } /** * Marks the read limit of the StringReader. * * @param limit the maximum limit of bytes that can be read before the mark position becomes * invalid */ public synchronized void mark(final int limit) { try { in.mark(limit); } catch (IOException ioe) { throw new RuntimeException(ioe.getMessage()); } } /** * @return the current number of bytes ready for reading * @exception IOException if an error occurs */ public synchronized int available() throws IOException { if (in == null) { throw new IOException("Stream Closed"); } if (slack != null) { return slack.length - begin; } if (in.ready()) { return 1; } else { return 0; } } /** @return false - mark is not supported */ public boolean markSupported() { return false; // would be imprecise } /** * Resets the StringReader. * * @exception IOException if the StringReader fails to be reset */ public synchronized void reset() throws IOException { if (in == null) { throw new IOException("Stream Closed"); } slack = null; in.reset(); } /** * Closes the Stringreader. * * @exception IOException if the original StringReader fails to be closed */ public synchronized void close() throws IOException { if (in != null) { in.close(); slack = null; in = null; } } /** Return the underlying Reader */ public Reader getReader() { return in; } }
public class MQTTHandler { private final Logger L = Logger.getLogger(getClass().getName()); public static void init() throws MqttException { instance = new MQTTHandler(); instance.doInit(); } private final String topicPrefix; private MQTTHandler() { String tp = System.getProperty("knx2mqtt.mqtt.topic", "knx"); if (!tp.endsWith("/")) tp += "/"; topicPrefix = tp; } private static MQTTHandler instance; private MqttClient mqttc; private void queueConnect() { shouldBeConnected = false; Main.t.schedule( new TimerTask() { @Override public void run() { doConnect(); } }, 10 * 1000); } private class StateChecker extends TimerTask { @Override public void run() { if (!mqttc.isConnected() && shouldBeConnected) { L.warning("Should be connected but aren't, reconnecting"); queueConnect(); } } } private boolean shouldBeConnected; private static boolean knxConnectionState; private void processSetGet(String namePart, MqttMessage msg, boolean set) { if (msg.isRetained()) { L.finer("Ignoring retained message " + msg + " to " + namePart); return; } // Now translate the topic into a group address GroupAddressInfo gai = GroupAddressManager.getGAInfoForName(namePart); if (gai == null) { L.warning( "Unable to translate name " + namePart + " into a group address, ignoring message " + msg); return; } L.fine("Name " + namePart + " translates to GA " + gai.address); String data = new String(msg.getPayload(), StandardCharsets.UTF_8); if (set) KNXConnector.doGroupWrite(gai.address, data, gai); else KNXConnector.doGroupRead(gai.address, data, gai); } void processMessage(String topic, MqttMessage msg) { L.fine("Received " + msg + " to " + topic); topic = topic.substring(topicPrefix.length(), topic.length()); if (topic.startsWith("set/")) processSetGet(topic.substring(4), msg, true); else if (topic.startsWith("get/")) processSetGet(topic.substring(4), msg, false); else L.warning("Ignored message " + msg + " to unknown topic " + topic); } private void doConnect() { L.info( "Connecting to MQTT broker " + mqttc.getServerURI() + " with CLIENTID=" + mqttc.getClientId() + " and TOPIC PREFIX=" + topicPrefix); MqttConnectOptions copts = new MqttConnectOptions(); copts.setWill(topicPrefix + "connected", "0".getBytes(), 1, true); copts.setCleanSession(true); copts.setUserName("emonpi"); copts.setPassword("emonpimqtt2016".toCharArray()); try { mqttc.connect(copts); sendConnectionState(); L.info("Successfully connected to broker, subscribing to " + topicPrefix + "(set|get)/#"); try { mqttc.subscribe(topicPrefix + "set/#", 1); mqttc.subscribe(topicPrefix + "get/#", 1); shouldBeConnected = true; } catch (MqttException mqe) { L.log(Level.WARNING, "Error subscribing to topic hierarchy, check your configuration", mqe); throw mqe; } } catch (MqttException mqe) { L.log( Level.WARNING, "Error while connecting to MQTT broker, will retry: " + mqe.getMessage(), mqe); queueConnect(); // Attempt reconnect } } private void doInit() throws MqttException { String server = System.getProperty("knx2mqtt.mqtt.server", "tcp://localhost:1883"); String clientID = System.getProperty("knx2mqtt.mqtt.clientid", "knx2mqtt"); mqttc = new MqttClient(server, clientID, new MemoryPersistence()); mqttc.setCallback( new MqttCallback() { @Override public void messageArrived(String topic, MqttMessage msg) throws Exception { try { processMessage(topic, msg); } catch (Exception e) { L.log(Level.WARNING, "Error when processing message " + msg + " for " + topic, e); } } @Override public void deliveryComplete(IMqttDeliveryToken token) { /* Intentionally ignored */ } @Override public void connectionLost(Throwable t) { L.log(Level.WARNING, "Connection to MQTT broker lost", t); queueConnect(); } }); doConnect(); Main.t.schedule(new StateChecker(), 30 * 1000, 30 * 1000); } private void doPublish( String name, Object val, String src, String dpt, String textual, long updateTime, long lastChange) { JsonObject jso = new JsonObject(); jso.add("ts", updateTime).add("lc", lastChange).add("knx_src_addr", src).add("knx_dpt", dpt); if (textual != null) jso.add("knx_textual", textual); if (val instanceof Integer) jso.add("val", ((Integer) val).intValue()); else if (val instanceof Number) jso.add("val", ((Number) val).doubleValue()); else jso.add("val", val.toString()); String txtmsg = jso.toString(); MqttMessage msg = new MqttMessage(jso.toString().getBytes(StandardCharsets.UTF_8)); msg.setQos(0); msg.setRetained(true); try { String fullTopic = topicPrefix + "status/" + name; mqttc.publish(fullTopic, msg); L.finer("Published " + txtmsg + " to " + fullTopic); } catch (MqttException e) { L.log(Level.WARNING, "Error when publishing message " + txtmsg, e); } } private void sendConnectionState() { try { instance.mqttc.publish( instance.topicPrefix + "connected", (knxConnectionState ? "2" : "1").getBytes(), 1, true); } catch (MqttException e) { /* Ignore */ } } public static void setKNXConnectionState(boolean connected) { knxConnectionState = connected; instance.sendConnectionState(); } public static void publish( String name, Object val, String src, String dpt, String textual, long updateTime, long lastChange) { instance.doPublish(name, val, src, dpt, textual, updateTime, lastChange); } }