]> git.basschouten.com Git - openhab-addons.git/blob
99e07e2efd486a9d5b0d85ff25a2575d38d8055d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.powermax.internal.message;
14
15 import java.util.ArrayList;
16 import java.util.Calendar;
17 import java.util.EventObject;
18 import java.util.GregorianCalendar;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.ConcurrentLinkedQueue;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
25
26 import org.openhab.binding.powermax.internal.connector.PowermaxConnector;
27 import org.openhab.binding.powermax.internal.connector.PowermaxSerialConnector;
28 import org.openhab.binding.powermax.internal.connector.PowermaxTcpConnector;
29 import org.openhab.binding.powermax.internal.state.PowermaxArmMode;
30 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings;
31 import org.openhab.binding.powermax.internal.state.PowermaxPanelType;
32 import org.openhab.binding.powermax.internal.state.PowermaxState;
33 import org.openhab.binding.powermax.internal.state.PowermaxStateEvent;
34 import org.openhab.binding.powermax.internal.state.PowermaxStateEventListener;
35 import org.openhab.core.common.ThreadPoolManager;
36 import org.openhab.core.io.transport.serial.SerialPortManager;
37 import org.openhab.core.types.Command;
38 import org.openhab.core.util.HexUtils;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
41
42 /**
43  * A class that manages the communication with the Visonic alarm system
44  *
45  * Visonic does not provide a specification of the RS232 protocol and, thus,
46  * the binding uses the available protocol specification given at the â€‹domoticaforum
47  * http://www.domoticaforum.eu/viewtopic.php?f=68&t=6581
48  *
49  * @author Laurent Garnier - Initial contribution
50  */
51 public class PowermaxCommManager implements PowermaxMessageEventListener {
52
53     private final Logger logger = LoggerFactory.getLogger(PowermaxCommManager.class);
54
55     private static final int DEFAULT_TCP_PORT = 80;
56     private static final int TCP_CONNECTION_TIMEOUT = 5000;
57     private static final int DEFAULT_BAUD_RATE = 9600;
58     private static final int WAITING_DELAY_FOR_RESPONSE = 750;
59     private static final long DELAY_BETWEEN_SETUP_DOWNLOADS = TimeUnit.SECONDS.toMillis(45);
60
61     private final ScheduledExecutorService scheduler;
62
63     /** The object to store the current settings of the Powermax alarm system */
64     private PowermaxPanelSettings panelSettings;
65
66     /** Panel type used when in standard mode */
67     private PowermaxPanelType panelType;
68
69     private boolean forceStandardMode;
70     private boolean autoSyncTime;
71
72     private List<PowermaxStateEventListener> listeners = new ArrayList<>();
73
74     /** The serial or TCP connecter used to communicate with the Powermax alarm system */
75     private PowermaxConnector connector;
76
77     /** The last message sent to the the Powermax alarm system */
78     private PowermaxBaseMessage lastSendMsg;
79
80     /** The message queue of messages to be sent to the the Powermax alarm system */
81     private ConcurrentLinkedQueue<PowermaxBaseMessage> msgQueue = new ConcurrentLinkedQueue<>();
82
83     /** The time in milliseconds the last download of the panel setup was requested */
84     private Long lastTimeDownloadRequested;
85
86     /** The boolean indicating if the download of the panel setup is in progress or not */
87     private boolean downloadRunning;
88
89     /** The time in milliseconds used to set time and date */
90     private Long syncTimeCheck;
91
92     /**
93      * Constructor for Serial Connection
94      *
95      * @param sPort the serial port name
96      * @param panelType the panel type to be used when in standard mode
97      * @param forceStandardMode true to force the standard mode rather than trying using the Powerlink mode
98      * @param autoSyncTime true for automatic sync time
99      * @param serialPortManager the serial port manager
100      * @param threadName the prefix name of threads to be created
101      */
102     public PowermaxCommManager(String sPort, PowermaxPanelType panelType, boolean forceStandardMode,
103             boolean autoSyncTime, SerialPortManager serialPortManager, String threadName) {
104         this.panelType = panelType;
105         this.forceStandardMode = forceStandardMode;
106         this.autoSyncTime = autoSyncTime;
107         this.panelSettings = new PowermaxPanelSettings(panelType);
108         this.scheduler = ThreadPoolManager.getScheduledPool(threadName + "-sender");
109         String serialPort = (sPort != null && !sPort.trim().isEmpty()) ? sPort.trim() : null;
110         if (serialPort != null) {
111             connector = new PowermaxSerialConnector(serialPortManager, serialPort, DEFAULT_BAUD_RATE,
112                     threadName + "-reader");
113         } else {
114             connector = null;
115         }
116     }
117
118     /**
119      * Constructor for TCP connection
120      *
121      * @param ip the IP address
122      * @param port TCP port number; default port is used if value <= 0
123      * @param panelType the panel type to be used when in standard mode
124      * @param forceStandardMode true to force the standard mode rather than trying using the Powerlink mode
125      * @param autoSyncTime true for automatic sync time
126      * @param serialPortManager
127      * @param threadName the prefix name of threads to be created
128      */
129     public PowermaxCommManager(String ip, int port, PowermaxPanelType panelType, boolean forceStandardMode,
130             boolean autoSyncTime, String threadName) {
131         this.panelType = panelType;
132         this.forceStandardMode = forceStandardMode;
133         this.autoSyncTime = autoSyncTime;
134         this.panelSettings = new PowermaxPanelSettings(panelType);
135         this.scheduler = ThreadPoolManager.getScheduledPool(threadName + "-sender");
136         String ipAddress = (ip != null && !ip.trim().isEmpty()) ? ip.trim() : null;
137         int tcpPort = (port > 0) ? port : DEFAULT_TCP_PORT;
138         if (ipAddress != null) {
139             connector = new PowermaxTcpConnector(ipAddress, tcpPort, TCP_CONNECTION_TIMEOUT, threadName + "-reader");
140         } else {
141             connector = null;
142         }
143     }
144
145     /**
146      * Add event listener
147      *
148      * @param listener the listener to be added
149      */
150     public synchronized void addEventListener(PowermaxStateEventListener listener) {
151         listeners.add(listener);
152         if (connector != null) {
153             connector.addEventListener(this);
154         }
155     }
156
157     /**
158      * Remove event listener
159      *
160      * @param listener the listener to be removed
161      */
162     public synchronized void removeEventListener(PowermaxStateEventListener listener) {
163         if (connector != null) {
164             connector.removeEventListener(this);
165         }
166         listeners.remove(listener);
167     }
168
169     /**
170      * Connect to the Powermax alarm system
171      *
172      * @return true if connected or false if not
173      */
174     public boolean open() {
175         if (connector != null) {
176             connector.open();
177         }
178         lastSendMsg = null;
179         msgQueue = new ConcurrentLinkedQueue<>();
180         return isConnected();
181     }
182
183     /**
184      * Close the connection to the Powermax alarm system.
185      *
186      * @return true if connected or false if not
187      */
188     public boolean close() {
189         if (connector != null) {
190             connector.close();
191         }
192         lastTimeDownloadRequested = null;
193         downloadRunning = false;
194         return isConnected();
195     }
196
197     /**
198      * @return true if connected to the Powermax alarm system or false if not
199      */
200     public boolean isConnected() {
201         return (connector != null) && connector.isConnected();
202     }
203
204     /**
205      * @return the current settings of the Powermax alarm system
206      */
207     public PowermaxPanelSettings getPanelSettings() {
208         return panelSettings;
209     }
210
211     /**
212      * Process and store all the panel settings from the raw buffers
213      *
214      * @param PowerlinkMode true if in Powerlink mode or false if in standard mode
215      *
216      * @return true if no problem encountered to get all the settings; false if not
217      */
218     public boolean processPanelSettings(boolean powerlinkMode) {
219         return panelSettings.process(powerlinkMode, panelType, powerlinkMode ? syncTimeCheck : null);
220     }
221
222     /**
223      * @return a new instance of PowermaxState
224      */
225     public PowermaxState createNewState() {
226         return new PowermaxState(panelSettings);
227     }
228
229     /**
230      * @return the last message sent to the Powermax alarm system
231      */
232     public synchronized PowermaxBaseMessage getLastSendMsg() {
233         return lastSendMsg;
234     }
235
236     @Override
237     public void onNewMessageEvent(EventObject event) {
238         PowermaxMessageEvent messageEvent = (PowermaxMessageEvent) event;
239         PowermaxBaseMessage message = messageEvent.getMessage();
240
241         if (logger.isDebugEnabled()) {
242             logger.debug("onNewMessageReceived(): received message 0x{} ({})",
243                     HexUtils.bytesToHex(message.getRawData()),
244                     (message.getReceiveType() != null) ? message.getReceiveType()
245                             : String.format("%02X", message.getCode()));
246         }
247
248         if (forceStandardMode && message instanceof PowermaxPowerlinkMessage) {
249             message = new PowermaxBaseMessage(message.getRawData());
250         }
251
252         PowermaxState updateState = message.handleMessage(this);
253         if (updateState != null) {
254             if (updateState.getUpdateSettings() != null) {
255                 panelSettings.updateRawSettings(updateState.getUpdateSettings());
256             }
257             if (!updateState.getUpdatedZoneNames().isEmpty()) {
258                 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
259                     panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx));
260                 }
261             }
262             if (!updateState.getUpdatedZoneInfos().isEmpty()) {
263                 for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) {
264                     panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx));
265                 }
266             }
267
268             PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState);
269
270             // send message to event listeners
271             for (int i = 0; i < listeners.size(); i++) {
272                 listeners.get(i).onNewStateEvent(newEvent);
273             }
274         }
275     }
276
277     /**
278      * Compute the CRC of a message
279      *
280      * @param data the buffer containing the message
281      * @param len the size of the message in the buffer
282      *
283      * @return the computed CRC
284      */
285     public static byte computeCRC(byte[] data, int len) {
286         long checksum = 0;
287         for (int i = 1; i < (len - 2); i++) {
288             checksum = checksum + (data[i] & 0x000000FF);
289         }
290         checksum = 0xFF - (checksum % 0xFF);
291         if (checksum == 0xFF) {
292             checksum = 0;
293         }
294         return (byte) checksum;
295     }
296
297     /**
298      * Send an ACK for a received message
299      *
300      * @param msg the received message object
301      * @param ackType the type of ACK to be sent
302      *
303      * @return true if the ACK was sent or false if not
304      */
305     public synchronized boolean sendAck(PowermaxBaseMessage msg, byte ackType) {
306         int code = msg.getCode();
307         byte[] rawData = msg.getRawData();
308         byte[] ackData;
309         if ((code >= 0x80) || ((code < 0x10) && (rawData[rawData.length - 3] == 0x43))) {
310             ackData = new byte[] { 0x0D, ackType, 0x43, 0x00, 0x0A };
311         } else {
312             ackData = new byte[] { 0x0D, ackType, 0x00, 0x0A };
313         }
314
315         if (logger.isDebugEnabled()) {
316             logger.debug("sendAck(): sending message {}", HexUtils.bytesToHex(ackData));
317         }
318         boolean done = sendMessage(ackData);
319         if (!done) {
320             logger.debug("sendAck(): failed");
321         }
322         return done;
323     }
324
325     /**
326      * Send a message to the Powermax alarm panel to change arm mode
327      *
328      * @param armMode the arm mode
329      * @param pinCode the PIN code. A string of 4 characters is expected
330      *
331      * @return true if the message was sent or false if not
332      */
333     public boolean requestArmMode(PowermaxArmMode armMode, String pinCode) {
334         logger.debug("requestArmMode(): armMode = {}", armMode.getShortName());
335
336         boolean done = false;
337         if (!armMode.isAllowedCommand()) {
338             logger.debug("Powermax alarm binding: requested arm mode {} rejected", armMode.getShortName());
339         } else if ((pinCode == null) || (pinCode.length() != 4)) {
340             logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
341                     armMode.getShortName());
342         } else {
343             try {
344                 byte[] dynPart = new byte[3];
345                 dynPart[0] = armMode.getCommandCode();
346                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
347                 dynPart[2] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
348
349                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.ARM, dynPart), false, 0, true);
350             } catch (NumberFormatException e) {
351                 logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
352                         armMode.getShortName());
353             }
354         }
355         return done;
356     }
357
358     /**
359      * Send a message to the Powermax alarm panel to change PGM or X10 zone state
360      *
361      * @param action the requested action. Allowed values are: OFF, ON, DIM, BRIGHT
362      * @param device the X10 device number. null is expected for PGM
363      *
364      * @return true if the message was sent or false if not
365      */
366     public boolean sendPGMX10(Command action, Byte device) {
367         logger.debug("sendPGMX10(): action = {}, device = {}", action, device);
368
369         boolean done = false;
370
371         Map<String, Byte> codes = new HashMap<>();
372         codes.put("OFF", (byte) 0x00);
373         codes.put("ON", (byte) 0x01);
374         codes.put("DIM", (byte) 0x0A);
375         codes.put("BRIGHT", (byte) 0x0B);
376
377         Byte code = codes.get(action.toString());
378         if (code == null) {
379             logger.debug("Powermax alarm binding: invalid PGM/X10 command: {}", action);
380         } else if ((device != null) && ((device < 1) || (device >= panelSettings.getNbPGMX10Devices()))) {
381             logger.debug("Powermax alarm binding: invalid X10 device id: {}", device);
382         } else {
383             int val = (device == null) ? 1 : (1 << device);
384             byte[] dynPart = new byte[3];
385             dynPart[0] = code;
386             dynPart[1] = (byte) (val & 0x000000FF);
387             dynPart[2] = (byte) (val >> 8);
388
389             done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.X10PGM, dynPart), false, 0);
390         }
391         return done;
392     }
393
394     /**
395      * Send a message to the Powermax alarm panel to bypass a zone or to not bypass a zone
396      *
397      * @param bypass true to bypass the zone; false to not bypass the zone
398      * @param zone the zone number (first zone is number 1)
399      * @param pinCode the PIN code. A string of 4 characters is expected
400      *
401      * @return true if the message was sent or false if not
402      */
403     public boolean sendZoneBypass(boolean bypass, byte zone, String pinCode) {
404         logger.debug("sendZoneBypass(): bypass = {}, zone = {}", bypass ? "true" : "false", zone);
405
406         boolean done = false;
407
408         if ((pinCode == null) || (pinCode.length() != 4)) {
409             logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
410         } else if ((zone < 1) || (zone > panelSettings.getNbZones())) {
411             logger.debug("Powermax alarm binding: invalid zone number: {}", zone);
412         } else {
413             try {
414                 int val = (1 << (zone - 1));
415
416                 byte[] dynPart = new byte[10];
417                 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
418                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
419                 int i;
420                 for (i = 2; i < 10; i++) {
421                     dynPart[i] = 0;
422                 }
423                 i = bypass ? 2 : 6;
424                 dynPart[i++] = (byte) (val & 0x000000FF);
425                 dynPart[i++] = (byte) ((val >> 8) & 0x000000FF);
426                 dynPart[i++] = (byte) ((val >> 16) & 0x000000FF);
427                 dynPart[i++] = (byte) ((val >> 24) & 0x000000FF);
428
429                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASS, dynPart), false, 0, true);
430                 if (done) {
431                     done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASSTAT), false, 0);
432                 }
433             } catch (NumberFormatException e) {
434                 logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
435             }
436         }
437         return done;
438     }
439
440     /**
441      * Send a message to set the alarm time and date using the system time and date
442      *
443      * @return true if the message was sent or false if not
444      */
445     public boolean sendSetTime() {
446         logger.debug("sendSetTime()");
447
448         boolean done = false;
449
450         if (autoSyncTime) {
451             GregorianCalendar cal = new GregorianCalendar();
452             if (cal.get(Calendar.YEAR) >= 2000) {
453                 logger.debug("sendSetTime(): sync time {}",
454                         String.format("%02d/%02d/%04d %02d:%02d:%02d", cal.get(Calendar.DAY_OF_MONTH),
455                                 cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR), cal.get(Calendar.HOUR_OF_DAY),
456                                 cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)));
457
458                 byte[] dynPart = new byte[6];
459                 dynPart[0] = (byte) cal.get(Calendar.SECOND);
460                 dynPart[1] = (byte) cal.get(Calendar.MINUTE);
461                 dynPart[2] = (byte) cal.get(Calendar.HOUR_OF_DAY);
462                 dynPart[3] = (byte) cal.get(Calendar.DAY_OF_MONTH);
463                 dynPart[4] = (byte) (cal.get(Calendar.MONTH) + 1);
464                 dynPart[5] = (byte) (cal.get(Calendar.YEAR) - 2000);
465
466                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.SETTIME, dynPart), false, 0);
467
468                 cal.set(Calendar.MILLISECOND, 0);
469                 syncTimeCheck = cal.getTimeInMillis();
470             } else {
471                 logger.info(
472                         "Powermax alarm binding: time not synchronized; please correct the date/time of your openHAB server");
473                 syncTimeCheck = null;
474             }
475         } else {
476             syncTimeCheck = null;
477         }
478         return done;
479     }
480
481     /**
482      * Send a message to the Powermax alarm panel to get all the event logs
483      *
484      * @param pinCode the PIN code. A string of 4 characters is expected
485      *
486      * @return true if the message was sent or false if not
487      */
488     public boolean requestEventLog(String pinCode) {
489         logger.debug("requestEventLog()");
490
491         boolean done = false;
492
493         if ((pinCode == null) || (pinCode.length() != 4)) {
494             logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
495         } else {
496             try {
497                 byte[] dynPart = new byte[3];
498                 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
499                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
500
501                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.EVENTLOG, dynPart), false, 0, true);
502             } catch (NumberFormatException e) {
503                 logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
504             }
505         }
506         return done;
507     }
508
509     /**
510      * Start downloading panel setup
511      *
512      * @return true if the message was sent or the sending is delayed; false in other cases
513      */
514     public synchronized boolean startDownload() {
515         if (downloadRunning) {
516             return false;
517         } else {
518             lastTimeDownloadRequested = System.currentTimeMillis();
519             downloadRunning = true;
520             return sendMessage(PowermaxSendType.DOWNLOAD);
521         }
522     }
523
524     /**
525      * Act the exit of the panel setup
526      */
527     public synchronized void exitDownload() {
528         downloadRunning = false;
529     }
530
531     public void retryDownloadSetup(int remainingAttempts) {
532         long now = System.currentTimeMillis();
533         if ((remainingAttempts > 0) && !isDownloadRunning() && ((lastTimeDownloadRequested == null)
534                 || ((now - lastTimeDownloadRequested) >= DELAY_BETWEEN_SETUP_DOWNLOADS))) {
535             // We wait at least 45 seconds before each retry to download the panel setup
536             logger.debug("Powermax alarm binding: try again downloading setup");
537             startDownload();
538         }
539     }
540
541     public void getInfosWhenInStandardMode() {
542         sendMessage(PowermaxSendType.ZONESNAME);
543         sendMessage(PowermaxSendType.ZONESTYPE);
544         sendMessage(PowermaxSendType.STATUS);
545     }
546
547     public void sendRestoreMessage() {
548         sendMessage(PowermaxSendType.RESTORE);
549     }
550
551     /**
552      * @return true if a download of the panel setup is in progress
553      */
554     public boolean isDownloadRunning() {
555         return downloadRunning;
556     }
557
558     /**
559      * @return the time in milliseconds the last download of the panel setup was requested or null if not yet requested
560      */
561     public Long getLastTimeDownloadRequested() {
562         return lastTimeDownloadRequested;
563     }
564
565     /**
566      * Send a ENROLL message
567      *
568      * @return true if the message was sent or the sending is delayed; false in other cases
569      */
570     public boolean enrollPowerlink() {
571         return sendMessage(new PowermaxBaseMessage(PowermaxSendType.ENROLL), true, 0);
572     }
573
574     /**
575      * Send a message or delay the sending if time frame for receiving response is not ended
576      *
577      * @param msgType the message type to be sent
578      *
579      * @return true if the message was sent or the sending is delayed; false in other cases
580      */
581     public boolean sendMessage(PowermaxSendType msgType) {
582         return sendMessage(new PowermaxBaseMessage(msgType), false, 0);
583     }
584
585     /**
586      * Delay the sending of a message
587      *
588      * @param msgType the message type to be sent
589      * @param waitTime the delay in seconds to wait
590      *
591      * @return true if the sending is delayed; false in other cases
592      */
593     public boolean sendMessageLater(PowermaxSendType msgType, int waitTime) {
594         return sendMessage(new PowermaxBaseMessage(msgType), false, waitTime);
595     }
596
597     private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime) {
598         return sendMessage(msg, immediate, waitTime, false);
599     }
600
601     /**
602      * Send a message or delay the sending if time frame for receiving response is not ended
603      *
604      * @param msg the message to be sent
605      * @param immediate true if the message has to be send without considering timing
606      * @param waitTime the delay in seconds to wait
607      * @param doNotLog true if the message contains data that must not be logged
608      *
609      * @return true if the message was sent or the sending is delayed; false in other cases
610      */
611     private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime,
612             boolean doNotLog) {
613         if ((waitTime > 0) && (msg != null)) {
614             logger.debug("sendMessage(): delay ({} s) sending message (type {})", waitTime, msg.getSendType());
615             // Don't queue the message
616             PowermaxBaseMessage msgToSendLater = new PowermaxBaseMessage(msg.getRawData());
617             msgToSendLater.setSendType(msg.getSendType());
618             scheduler.schedule(() -> {
619                 sendMessage(msgToSendLater, false, 0);
620             }, waitTime, TimeUnit.SECONDS);
621             return true;
622         }
623
624         if (msg == null) {
625             msg = msgQueue.peek();
626             if (msg == null) {
627                 logger.debug("sendMessage(): nothing to send");
628                 return false;
629             }
630         }
631
632         // Delay sending if time frame for receiving response is not ended
633         long delay = WAITING_DELAY_FOR_RESPONSE - (System.currentTimeMillis() - connector.getWaitingForResponse());
634
635         PowermaxBaseMessage msgToSend = msg;
636
637         if (!immediate) {
638             msgToSend = msgQueue.peek();
639             if (msgToSend != msg) {
640                 logger.debug("sendMessage(): add message in queue (type {})", msg.getSendType());
641                 msgQueue.offer(msg);
642                 msgToSend = msgQueue.peek();
643             }
644             if ((msgToSend != msg) && (delay > 0)) {
645                 return true;
646             } else if ((msgToSend == msg) && (delay > 0)) {
647                 if (delay < 100) {
648                     delay = 100;
649                 }
650                 logger.debug("sendMessage(): delay ({} ms) sending message (type {})", delay, msgToSend.getSendType());
651                 scheduler.schedule(() -> {
652                     sendMessage(null, false, 0);
653                 }, delay, TimeUnit.MILLISECONDS);
654                 return true;
655             } else {
656                 msgToSend = msgQueue.poll();
657             }
658         }
659
660         if (logger.isDebugEnabled()) {
661             logger.debug("sendMessage(): sending {} message {}", msgToSend.getSendType(),
662                     doNotLog ? "***" : HexUtils.bytesToHex(msgToSend.getRawData()));
663         }
664         boolean done = sendMessage(msgToSend.getRawData());
665         if (done) {
666             lastSendMsg = msgToSend;
667             connector.setWaitingForResponse(System.currentTimeMillis());
668
669             if (!immediate && (msgQueue.peek() != null)) {
670                 logger.debug("sendMessage(): delay sending next message (type {})", msgQueue.peek().getSendType());
671                 scheduler.schedule(() -> {
672                     sendMessage(null, false, 0);
673                 }, WAITING_DELAY_FOR_RESPONSE, TimeUnit.MILLISECONDS);
674             }
675         } else {
676             logger.debug("sendMessage(): failed");
677         }
678
679         return done;
680     }
681
682     /**
683      * Send a message to the Powermax alarm panel
684      *
685      * @param data the data buffer containing the message to be sent
686      *
687      * @return true if the message was sent or false if not
688      */
689     private boolean sendMessage(byte[] data) {
690         boolean done = false;
691         if (isConnected()) {
692             data[data.length - 2] = computeCRC(data, data.length);
693             connector.sendMessage(data);
694             done = connector.isConnected();
695         } else {
696             logger.debug("sendMessage(): aborted (not connected)");
697         }
698         return done;
699     }
700 }