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