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