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.pentair.internal.handler;
15 import static org.openhab.binding.pentair.internal.PentairBindingConstants.INTELLIFLO_THING_TYPE;
17 import java.io.BufferedInputStream;
18 import java.io.BufferedOutputStream;
19 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.openhab.binding.pentair.internal.PentairPacket;
26 import org.openhab.binding.pentair.internal.PentairPacketIntellichlor;
27 import org.openhab.core.thing.Bridge;
28 import org.openhab.core.thing.ChannelUID;
29 import org.openhab.core.thing.Thing;
30 import org.openhab.core.thing.ThingStatus;
31 import org.openhab.core.thing.ThingStatusDetail;
32 import org.openhab.core.thing.binding.BaseBridgeHandler;
33 import org.openhab.core.types.Command;
34 import org.openhab.core.types.RefreshType;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * Abstract class for all common functions for different bridge implementations. Use as superclass for IPBridge and
40 * SerialBridge implementations.
42 * - Implements parsing of packets on Pentair bus and dispositions to appropriate Thing
43 * - Periodically sends query to any {@link PentairIntelliFloHandler} things
44 * - Provides function to write packets
46 * @author Jeff James - Initial contribution
49 public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler {
50 private final Logger logger = LoggerFactory.getLogger(PentairBaseBridgeHandler.class);
52 /** input stream - subclass needs to assign in connect function */
53 protected BufferedInputStream reader;
54 /** output stream - subclass needs to assing in connect function */
55 protected BufferedOutputStream writer;
56 /** thread for parser - subclass needs to create/assign connect */
57 protected Thread thread;
58 /** parser object - subclass needs to create/assign during connect */
59 protected Parser parser;
60 /** polling job for pump status */
61 protected ScheduledFuture<?> pollingjob;
62 /** ID to use when sending commands on Pentair bus - subclass needs to assign based on configuration parameter */
64 /** array to keep track of IDs seen on the Pentair bus that do not correlate to a configured Thing object */
65 protected ArrayList<Integer> unregistered = new ArrayList<>();
76 private enum ParserState {
87 PentairBaseBridgeHandler(Bridge bridge) {
92 public void handleCommand(ChannelUID channelUID, Command command) {
93 if (command instanceof RefreshType) {
94 logger.debug("Bridge received refresh command");
99 public void initialize() {
100 logger.debug("initializing Pentair Bridge handler.");
104 pollingjob = scheduler.scheduleWithFixedDelay(new PumpStatus(), 10, 120, TimeUnit.SECONDS);
108 public void dispose() {
109 logger.debug("Handler disposed.");
110 pollingjob.cancel(true);
115 * Abstract method for creating connection. Must be implemented in subclass.
117 protected abstract void connect();
120 * Abstract method for disconnect. Must be implemented in subclass
122 protected abstract void disconnect();
125 * Helper function to find a Thing assigned to this bridge with a specific pentair bus id.
127 * @param id Pentiar bus id
128 * @return Thing object. null if id is not found.
130 public Thing findThing(int id) {
131 List<Thing> things = getThing().getThings();
133 for (Thing t : things) {
134 PentairBaseThingHandler handler = (PentairBaseThingHandler) t.getHandler();
136 if (handler != null && handler.getPentairID() == id) {
145 * Class for throwing an End of Buffer exception, used in getByte when read returns a -1. This is used to signal an
146 * exit from the parser.
148 * @author Jeff James - initial contribution
151 public class EOBException extends Exception {
152 private static final long serialVersionUID = 1L;
156 * Gets a single byte from reader input stream
158 * @param s used during debug to identify proper state transitioning
159 * @return next byte from reader
160 * @throws EOBException
161 * @throws IOException
163 private int getByte(ParserState s) throws EOBException, IOException {
168 // EOBException is thrown if no more bytes in buffer. This exception is used to exit the parser when full
169 // packet is not in buffer
170 throw new EOBException();
177 * Gets a specific number of bytes from reader input stream
179 * @param buf byte buffer to store bytes
180 * @param start starting index to store bytes
181 * @param n number of bytes to read
182 * @return number of bytes read
183 * @throws EOBException
184 * @throws IOException
186 private int getBytes(byte[] buf, int start, int n) throws EOBException, IOException {
190 for (i = 0; i < n; i++) {
193 // EOBException is thrown if no more bytes in buffer. This exception is used to exit the parser when
194 // full packet is not in buffer
195 throw new EOBException();
198 buf[start + i] = (byte) c;
205 * Job to send pump query status packages to all Intelliflo Pump things in order to see the status.
206 * Note: From the internet is seems some FW versions of EasyTouch controllers send this automatically and this the
207 * pump status packets can just be snooped, however my controller version does not do this. No harm in sending.
212 class PumpStatus implements Runnable {
215 List<Thing> things = getThing().getThings();
217 // FF 00 FF A5 00 60 10 07 00 01 1C
218 byte[] packet = { (byte) 0xA5, (byte) 0x00, (byte) 0x00, (byte) id, (byte) 0x07, (byte) 0x00 };
220 PentairPacket p = new PentairPacket(packet);
222 for (Thing t : things) {
223 if (!t.getThingTypeUID().equals(INTELLIFLO_THING_TYPE)) {
227 p.setDest(((PentairIntelliFloHandler) t.getHandler()).id);
230 Thread.sleep(300); // make sure each pump has time to respond
231 } catch (InterruptedException e) {
239 * Implements the thread to read and parse the input stream. Once a packet can be indentified, it locates the
240 * representive sending Thing and dispositions the packet so it can be further processed.
242 * @author Jeff James - initial implementation
245 class Parser implements Runnable {
248 logger.debug("parser thread started");
249 byte[] buf = new byte[40];
251 int chksum, i, length;
253 PentairBaseThingHandler thinghandler;
255 ParserState parserstate = ParserState.WAIT_SOC;
258 while (!Thread.currentThread().isInterrupted()) {
259 c = getByte(parserstate);
261 switch (parserstate) {
263 if (c == 0xFF) { // for CMD_PENTAIR, we need at lease one 0xFF
265 c = getByte(parserstate);
266 } while (c == 0xFF); // consume all 0xFF
269 parserstate = ParserState.CMD_PENTAIR;
274 parserstate = ParserState.CMD_INTELLICHLOR;
278 parserstate = ParserState.WAIT_SOC; // any break will go back to WAIT_SOC
281 logger.debug("FF00 !FF");
285 if (getBytes(buf, 0, 6) != 6) { // read enough to get the length
286 logger.debug("Unable to read 6 bytes");
290 if (buf[0] != (byte) 0xA5) {
291 logger.debug("FF00FF !A5");
297 logger.debug("Command length of 0");
300 logger.debug("Received packet longer than 34 bytes: {}", length);
303 if (getBytes(buf, 6, length) != length) { // read remaining packet
308 for (i = 0; i < length + 6; i++) {
309 chksum += buf[i] & 0xFF;
312 c = getByte(parserstate) << 8;
313 c += getByte(parserstate);
316 logger.debug("Checksum error: {}", PentairPacket.bytesToHex(buf, length + 6));
320 PentairPacket p = new PentairPacket(buf);
322 thing = findThing(p.getSource());
324 if ((p.getSource() >> 8) == 0x02) { // control panels are 0x3*, don't treat as an
325 // unregistered device
326 logger.trace("Command from control panel device ({}): {}", p.getSource(), p);
327 } else if (!unregistered.contains(p.getSource())) { // if not yet seen, print out log
329 logger.info("Command from unregistered device ({}): {}", p.getSource(), p);
330 unregistered.add(p.getSource());
332 logger.trace("Command from unregistered device ({}): {}", p.getSource(), p);
337 thinghandler = (PentairBaseThingHandler) thing.getHandler();
338 if (thinghandler == null) {
339 logger.debug("Thing handler = null");
343 logger.trace("Received pentair command: {}", p);
345 thinghandler.processPacketFrom(p);
348 case CMD_INTELLICHLOR:
349 parserstate = ParserState.WAIT_SOC;
351 buf[0] = 0x10; // 0x10 is included in checksum
352 if (c != (byte) 0x02) {
358 // assume 3 byte command, plus 1 checksum, plus 0x10, 0x03
359 if (getBytes(buf, 2, 6) != 6) {
363 // Check to see if this is a 3 or 4 byte command
364 if ((buf[6] != (byte) 0x10 || buf[7] != (byte) 0x03)) {
367 buf[8] = (byte) getByte(parserstate);
368 if ((buf[7] != (byte) 0x10) && (buf[8] != (byte) 0x03)) {
369 logger.debug("Invalid Intellichlor command: {}",
370 PentairPacket.bytesToHex(buf, length + 6));
371 break; // invalid command
376 for (i = 0; i < length + 2; i++) {
377 chksum += buf[i] & 0xFF;
380 c = buf[length + 2] & 0xFF;
381 if (c != (chksum & 0xFF)) { // make sure it matches chksum
382 logger.debug("Invalid Intellichlor checksum: {}",
383 PentairPacket.bytesToHex(buf, length + 6));
387 PentairPacketIntellichlor pic = new PentairPacketIntellichlor(buf, length);
389 thing = findThing(0);
392 if (!unregistered.contains(0)) { // if not yet seen, print out log message
393 logger.info("Command from unregistered Intelliflow: {}", pic);
396 logger.trace("Command from unregistered Intelliflow: {}", pic);
402 thinghandler = (PentairBaseThingHandler) thing.getHandler();
403 if (thinghandler == null) {
404 logger.debug("Thing handler = null");
408 thinghandler.processPacketFrom(pic);
413 } catch (IOException e) {
414 logger.trace("I/O error while reading from stream: {}", e.getMessage());
416 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
417 } catch (EOBException e) {
418 // EOB Exception is used to exit the parser loop if full message is not in buffer.
421 logger.debug("msg reader thread exited");
426 * Method to write a package on the Pentair bus. Will add preamble and checksum to bytes written
428 * @param p {@link PentairPacket} to write
430 public void writePacket(PentairPacket p) {
431 try { // FF 00 FF A5 00 60 10 07 00 01 1C
432 byte[] preamble = { (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0x00, (byte) 0xFF };
433 byte[] buf = new byte[5 + p.getLength() + 8]; // 5 is preamble, 8 is 6 bytes for header and 2 for checksum
437 System.arraycopy(preamble, 0, buf, 0, 5);
438 System.arraycopy(p.buf, 0, buf, 5, p.getLength() + 6);
439 int checksum = p.calcChecksum();
441 buf[p.getLength() + 11] = (byte) ((checksum >> 8) & 0xFF);
442 buf[p.getLength() + 12] = (byte) (checksum & 0xFF);
444 logger.debug("Writing packet: {}", PentairPacket.bytesToHex(buf));
446 writer.write(buf, 0, 5 + p.getLength() + 8);
448 } catch (IOException e) {
449 logger.trace("I/O error while writing stream", e);
450 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());