]> git.basschouten.com Git - openhab-addons.git/blob
ab3f0f84a5277c9e6c107a502af21b121db58c64
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.pentair.internal.handler;
14
15 import static org.openhab.binding.pentair.internal.PentairBindingConstants.INTELLIFLO_THING_TYPE;
16
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;
24
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;
37
38 /**
39  * Abstract class for all common functions for different bridge implementations. Use as superclass for IPBridge and
40  * SerialBridge implementations.
41  *
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
45  *
46  * @author Jeff James - Initial contribution
47  *
48  */
49 public abstract class PentairBaseBridgeHandler extends BaseBridgeHandler {
50     private final Logger logger = LoggerFactory.getLogger(PentairBaseBridgeHandler.class);
51
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 */
63     protected int id;
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<>();
66
67     /**
68      * Gets pentair bus id
69      *
70      * @return id
71      */
72     public int getId() {
73         return id;
74     }
75
76     private enum ParserState {
77         WAIT_SOC,
78         CMD_PENTAIR,
79         CMD_INTELLICHLOR
80     }
81
82     /**
83      * Constructor
84      *
85      * @param bridge
86      */
87     PentairBaseBridgeHandler(Bridge bridge) {
88         super(bridge);
89     }
90
91     @Override
92     public void handleCommand(ChannelUID channelUID, Command command) {
93         if (command instanceof RefreshType) {
94             logger.debug("Bridge received refresh command");
95         }
96     }
97
98     @Override
99     public void initialize() {
100         logger.debug("initializing Pentair Bridge handler.");
101
102         connect();
103
104         pollingjob = scheduler.scheduleWithFixedDelay(new PumpStatus(), 10, 120, TimeUnit.SECONDS);
105     }
106
107     @Override
108     public void dispose() {
109         logger.debug("Handler disposed.");
110         pollingjob.cancel(true);
111         disconnect();
112     }
113
114     /**
115      * Abstract method for creating connection. Must be implemented in subclass.
116      */
117     protected abstract void connect();
118
119     /**
120      * Abstract method for disconnect. Must be implemented in subclass
121      */
122     protected abstract void disconnect();
123
124     /**
125      * Helper function to find a Thing assigned to this bridge with a specific pentair bus id.
126      *
127      * @param id Pentiar bus id
128      * @return Thing object. null if id is not found.
129      */
130     public Thing findThing(int id) {
131         List<Thing> things = getThing().getThings();
132
133         for (Thing t : things) {
134             PentairBaseThingHandler handler = (PentairBaseThingHandler) t.getHandler();
135
136             if (handler != null && handler.getPentairID() == id) {
137                 return t;
138             }
139         }
140
141         return null;
142     }
143
144     /**
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.
147      *
148      * @author Jeff James - initial contribution
149      *
150      */
151     public class EOBException extends Exception {
152         private static final long serialVersionUID = 1L;
153     }
154
155     /**
156      * Gets a single byte from reader input stream
157      *
158      * @param s used during debug to identify proper state transitioning
159      * @return next byte from reader
160      * @throws EOBException
161      * @throws IOException
162      */
163     private int getByte(ParserState s) throws EOBException, IOException {
164         int c = 0;
165
166         c = reader.read();
167         if (c == -1) {
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();
171         }
172
173         return c;
174     }
175
176     /**
177      * Gets a specific number of bytes from reader input stream
178      *
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
185      */
186     private int getBytes(byte[] buf, int start, int n) throws EOBException, IOException {
187         int i;
188         int c;
189
190         for (i = 0; i < n; i++) {
191             c = reader.read();
192             if (c == -1) {
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();
196             }
197
198             buf[start + i] = (byte) c;
199         }
200
201         return i;
202     }
203
204     /**
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.
208      *
209      * @author Jeff James
210      *
211      */
212     class PumpStatus implements Runnable {
213         @Override
214         public void run() {
215             List<Thing> things = getThing().getThings();
216
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 };
219
220             PentairPacket p = new PentairPacket(packet);
221
222             for (Thing t : things) {
223                 if (!t.getThingTypeUID().equals(INTELLIFLO_THING_TYPE)) {
224                     continue;
225                 }
226
227                 p.setDest(((PentairIntelliFloHandler) t.getHandler()).id);
228                 writePacket(p);
229                 try {
230                     Thread.sleep(300); // make sure each pump has time to respond
231                 } catch (InterruptedException e) {
232                     break;
233                 }
234             }
235         }
236     }
237
238     /**
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.
241      *
242      * @author Jeff James - initial implementation
243      *
244      */
245     class Parser implements Runnable {
246         @Override
247         public void run() {
248             logger.debug("parser thread started");
249             byte buf[] = new byte[40];
250             int c;
251             int chksum, i, length;
252             Thing thing;
253             PentairBaseThingHandler thinghandler;
254
255             ParserState parserstate = ParserState.WAIT_SOC;
256
257             try {
258                 while (!Thread.currentThread().isInterrupted()) {
259                     c = getByte(parserstate);
260
261                     switch (parserstate) {
262                         case WAIT_SOC:
263                             if (c == 0xFF) { // for CMD_PENTAIR, we need at lease one 0xFF
264                                 do {
265                                     c = getByte(parserstate);
266                                 } while (c == 0xFF); // consume all 0xFF
267
268                                 if (c == 0x00) {
269                                     parserstate = ParserState.CMD_PENTAIR;
270                                 }
271                             }
272
273                             if (c == 0x10) {
274                                 parserstate = ParserState.CMD_INTELLICHLOR;
275                             }
276                             break;
277                         case CMD_PENTAIR:
278                             parserstate = ParserState.WAIT_SOC; // any break will go back to WAIT_SOC
279
280                             if (c != 0xFF) {
281                                 logger.debug("FF00 !FF");
282                                 break;
283                             }
284
285                             if (getBytes(buf, 0, 6) != 6) { // read enough to get the length
286                                 logger.debug("Unable to read 6 bytes");
287
288                                 break;
289                             }
290                             if (buf[0] != (byte) 0xA5) {
291                                 logger.debug("FF00FF !A5");
292                                 break;
293                             }
294
295                             length = buf[5];
296                             if (length == 0) {
297                                 logger.debug("Command length of 0");
298                             }
299                             if (length > 34) {
300                                 logger.debug("Received packet longer than 34 bytes: {}", length);
301                                 break;
302                             }
303                             if (getBytes(buf, 6, length) != length) { // read remaining packet
304                                 break;
305                             }
306
307                             chksum = 0;
308                             for (i = 0; i < length + 6; i++) {
309                                 chksum += buf[i] & 0xFF;
310                             }
311
312                             c = getByte(parserstate) << 8;
313                             c += getByte(parserstate);
314
315                             if (c != chksum) {
316                                 logger.debug("Checksum error: {}", PentairPacket.bytesToHex(buf, length + 6));
317                                 break;
318                             }
319
320                             PentairPacket p = new PentairPacket(buf);
321
322                             thing = findThing(p.getSource());
323                             if (thing == null) {
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
328                                                                                     // message once
329                                     logger.info("Command from unregistered device ({}): {}", p.getSource(), p);
330                                     unregistered.add(p.getSource());
331                                 } else {
332                                     logger.trace("Command from unregistered device ({}): {}", p.getSource(), p);
333                                 }
334                                 break;
335                             }
336
337                             thinghandler = (PentairBaseThingHandler) thing.getHandler();
338                             if (thinghandler == null) {
339                                 logger.debug("Thing handler = null");
340                                 break;
341                             }
342
343                             logger.trace("Received pentair command: {}", p);
344
345                             thinghandler.processPacketFrom(p);
346
347                             break;
348                         case CMD_INTELLICHLOR:
349                             parserstate = ParserState.WAIT_SOC;
350
351                             buf[0] = 0x10; // 0x10 is included in checksum
352                             if (c != (byte) 0x02) {
353                                 break;
354                             }
355
356                             buf[1] = 0x2;
357                             length = 3;
358                             // assume 3 byte command, plus 1 checksum, plus 0x10, 0x03
359                             if (getBytes(buf, 2, 6) != 6) {
360                                 break;
361                             }
362
363                             // Check to see if this is a 3 or 4 byte command
364                             if ((buf[6] != (byte) 0x10 || buf[7] != (byte) 0x03)) {
365                                 length = 4;
366
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
372                                 }
373                             }
374
375                             chksum = 0;
376                             for (i = 0; i < length + 2; i++) {
377                                 chksum += buf[i] & 0xFF;
378                             }
379
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));
384                                 break;
385                             }
386
387                             PentairPacketIntellichlor pic = new PentairPacketIntellichlor(buf, length);
388
389                             thing = findThing(0);
390
391                             if (thing == null) {
392                                 if (!unregistered.contains(0)) { // if not yet seen, print out log message
393                                     logger.info("Command from unregistered Intelliflow: {}", pic);
394                                     unregistered.add(0);
395                                 } else {
396                                     logger.trace("Command from unregistered Intelliflow: {}", pic);
397                                 }
398
399                                 break;
400                             }
401
402                             thinghandler = (PentairBaseThingHandler) thing.getHandler();
403                             if (thinghandler == null) {
404                                 logger.debug("Thing handler = null");
405                                 break;
406                             }
407
408                             thinghandler.processPacketFrom(pic);
409
410                             break;
411                     }
412                 }
413             } catch (IOException e) {
414                 logger.trace("I/O error while reading from stream: {}", e.getMessage());
415                 disconnect();
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.
419             }
420
421             logger.debug("msg reader thread exited");
422         }
423     }
424
425     /**
426      * Method to write a package on the Pentair bus. Will add preamble and checksum to bytes written
427      *
428      * @param p {@link PentairPacket} to write
429      */
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
434
435             p.setSource(id);
436
437             System.arraycopy(preamble, 0, buf, 0, 5);
438             System.arraycopy(p.buf, 0, buf, 5, p.getLength() + 6);
439             int checksum = p.calcChecksum();
440
441             buf[p.getLength() + 11] = (byte) ((checksum >> 8) & 0xFF);
442             buf[p.getLength() + 12] = (byte) (checksum & 0xFF);
443
444             logger.debug("Writing packet: {}", PentairPacket.bytesToHex(buf));
445
446             writer.write(buf, 0, 5 + p.getLength() + 8);
447             writer.flush();
448         } catch (IOException e) {
449             logger.trace("I/O error while writing stream", e);
450             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
451         }
452     }
453 }