]> git.basschouten.com Git - openhab-addons.git/blob
175f6879d1c53c020fe8c76ad7c6c35b84d240b5
[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 void open() throws Exception {
180         if (connector != null) {
181             connector.open();
182         }
183         lastSendMsg = null;
184         msgQueue = new ConcurrentLinkedQueue<>();
185     }
186
187     /**
188      * Close the connection to the Powermax alarm system.
189      *
190      * @return true if connected or false if not
191      */
192     public boolean close() {
193         if (connector != null) {
194             connector.close();
195         }
196         lastTimeDownloadRequested = null;
197         downloadRunning = false;
198         return isConnected();
199     }
200
201     /**
202      * @return true if connected to the Powermax alarm system or false if not
203      */
204     public boolean isConnected() {
205         return (connector != null) && connector.isConnected();
206     }
207
208     /**
209      * @return the current settings of the Powermax alarm system
210      */
211     public PowermaxPanelSettings getPanelSettings() {
212         return panelSettings;
213     }
214
215     /**
216      * Process and store all the panel settings from the raw buffers
217      *
218      * @param PowerlinkMode true if in Powerlink mode or false if in standard mode
219      *
220      * @return true if no problem encountered to get all the settings; false if not
221      */
222     public boolean processPanelSettings(boolean powerlinkMode) {
223         return panelSettings.process(powerlinkMode, panelType, powerlinkMode ? syncTimeCheck : null);
224     }
225
226     /**
227      * @return a new instance of PowermaxState
228      */
229     public PowermaxState createNewState() {
230         return new PowermaxState(panelSettings, timeZoneProvider);
231     }
232
233     /**
234      * @return the last message sent to the Powermax alarm system
235      */
236     public synchronized PowermaxBaseMessage getLastSendMsg() {
237         return lastSendMsg;
238     }
239
240     @Override
241     public void onNewMessageEvent(EventObject event) {
242         PowermaxMessageEvent messageEvent = (PowermaxMessageEvent) event;
243         PowermaxBaseMessage message = messageEvent.getMessage();
244
245         if (logger.isDebugEnabled()) {
246             logger.debug("onNewMessageReceived(): received message 0x{} ({})",
247                     HexUtils.bytesToHex(message.getRawData()),
248                     (message.getReceiveType() != null) ? message.getReceiveType()
249                             : String.format("%02X", message.getCode()));
250         }
251
252         if (forceStandardMode && message instanceof PowermaxPowerlinkMessage) {
253             message = new PowermaxBaseMessage(message.getRawData());
254         }
255
256         PowermaxState updateState = message.handleMessage(this);
257
258         if (updateState == null) {
259             updateState = createNewState();
260         }
261
262         updateState.lastMessageTime.setValue(System.currentTimeMillis());
263
264         if (updateState.getUpdateSettings() != null) {
265             panelSettings.updateRawSettings(updateState.getUpdateSettings());
266         }
267         if (!updateState.getUpdatedZoneNames().isEmpty()) {
268             for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
269                 panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx));
270             }
271         }
272         if (!updateState.getUpdatedZoneInfos().isEmpty()) {
273             for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) {
274                 panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx));
275             }
276         }
277
278         PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState);
279
280         // send message to event listeners
281         listeners.forEach(listener -> listener.onNewStateEvent(newEvent));
282     }
283
284     @Override
285     public void onCommunicationFailure(String message) {
286         close();
287         listeners.forEach(listener -> listener.onCommunicationFailure(message));
288     }
289
290     /**
291      * Compute the CRC of a message
292      *
293      * @param data the buffer containing the message
294      * @param len the size of the message in the buffer
295      *
296      * @return the computed CRC
297      */
298     public static byte computeCRC(byte[] data, int len) {
299         long checksum = 0;
300         for (int i = 1; i < (len - 2); i++) {
301             checksum = checksum + (data[i] & 0x000000FF);
302         }
303         checksum = 0xFF - (checksum % 0xFF);
304         if (checksum == 0xFF) {
305             checksum = 0;
306         }
307         return (byte) checksum;
308     }
309
310     /**
311      * Send an ACK for a received message
312      *
313      * @param msg the received message object
314      * @param ackType the type of ACK to be sent
315      *
316      * @return true if the ACK was sent or false if not
317      */
318     public synchronized boolean sendAck(PowermaxBaseMessage msg, byte ackType) {
319         int code = msg.getCode();
320         byte[] rawData = msg.getRawData();
321         byte[] ackData;
322         if ((code >= 0x80) || ((code < 0x10) && (rawData[rawData.length - 3] == 0x43))) {
323             ackData = new byte[] { 0x0D, ackType, 0x43, 0x00, 0x0A };
324         } else {
325             ackData = new byte[] { 0x0D, ackType, 0x00, 0x0A };
326         }
327
328         if (logger.isDebugEnabled()) {
329             logger.debug("sendAck(): sending message {}", HexUtils.bytesToHex(ackData));
330         }
331         boolean done = sendMessage(ackData);
332         if (!done) {
333             logger.debug("sendAck(): failed");
334         }
335         return done;
336     }
337
338     /**
339      * Send a message to the Powermax alarm panel to change arm mode
340      *
341      * @param armMode the arm mode
342      * @param pinCode the PIN code. A string of 4 characters is expected
343      *
344      * @return true if the message was sent or false if not
345      */
346     public boolean requestArmMode(PowermaxArmMode armMode, String pinCode) {
347         logger.debug("requestArmMode(): armMode = {}", armMode.getShortName());
348
349         boolean done = false;
350         if (!armMode.isAllowedCommand()) {
351             logger.debug("Powermax alarm binding: requested arm mode {} rejected", armMode.getShortName());
352         } else if ((pinCode == null) || (pinCode.length() != 4)) {
353             logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
354                     armMode.getShortName());
355         } else {
356             try {
357                 byte[] dynPart = new byte[3];
358                 dynPart[0] = armMode.getCommandCode();
359                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
360                 dynPart[2] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
361
362                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.ARM, dynPart), false, 0, true);
363             } catch (NumberFormatException e) {
364                 logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
365                         armMode.getShortName());
366             }
367         }
368         return done;
369     }
370
371     /**
372      * Send a message to the Powermax alarm panel to change PGM or X10 zone state
373      *
374      * @param action the requested action. Allowed values are: OFF, ON, DIM, BRIGHT
375      * @param device the X10 device number. null is expected for PGM
376      *
377      * @return true if the message was sent or false if not
378      */
379     public boolean sendPGMX10(Command action, Byte device) {
380         logger.debug("sendPGMX10(): action = {}, device = {}", action, device);
381
382         boolean done = false;
383
384         Map<String, Byte> codes = new HashMap<>();
385         codes.put("OFF", (byte) 0x00);
386         codes.put("ON", (byte) 0x01);
387         codes.put("DIM", (byte) 0x0A);
388         codes.put("BRIGHT", (byte) 0x0B);
389
390         Byte code = codes.get(action.toString());
391         if (code == null) {
392             logger.debug("Powermax alarm binding: invalid PGM/X10 command: {}", action);
393         } else if ((device != null) && ((device < 1) || (device >= panelSettings.getNbPGMX10Devices()))) {
394             logger.debug("Powermax alarm binding: invalid X10 device id: {}", device);
395         } else {
396             int val = (device == null) ? 1 : (1 << device);
397             byte[] dynPart = new byte[3];
398             dynPart[0] = code;
399             dynPart[1] = (byte) (val & 0x000000FF);
400             dynPart[2] = (byte) (val >> 8);
401
402             done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.X10PGM, dynPart), false, 0);
403         }
404         return done;
405     }
406
407     /**
408      * Send a message to the Powermax alarm panel to bypass a zone or to not bypass a zone
409      *
410      * @param bypass true to bypass the zone; false to not bypass the zone
411      * @param zone the zone number (first zone is number 1)
412      * @param pinCode the PIN code. A string of 4 characters is expected
413      *
414      * @return true if the message was sent or false if not
415      */
416     public boolean sendZoneBypass(boolean bypass, byte zone, String pinCode) {
417         logger.debug("sendZoneBypass(): bypass = {}, zone = {}", bypass ? "true" : "false", zone);
418
419         boolean done = false;
420
421         if ((pinCode == null) || (pinCode.length() != 4)) {
422             logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
423         } else if ((zone < 1) || (zone > panelSettings.getNbZones())) {
424             logger.debug("Powermax alarm binding: invalid zone number: {}", zone);
425         } else {
426             try {
427                 int val = (1 << (zone - 1));
428
429                 byte[] dynPart = new byte[10];
430                 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
431                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
432                 int i;
433                 for (i = 2; i < 10; i++) {
434                     dynPart[i] = 0;
435                 }
436                 i = bypass ? 2 : 6;
437                 dynPart[i++] = (byte) (val & 0x000000FF);
438                 dynPart[i++] = (byte) ((val >> 8) & 0x000000FF);
439                 dynPart[i++] = (byte) ((val >> 16) & 0x000000FF);
440                 dynPart[i++] = (byte) ((val >> 24) & 0x000000FF);
441
442                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASS, dynPart), false, 0, true);
443                 if (done) {
444                     done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASSTAT), false, 0);
445                 }
446             } catch (NumberFormatException e) {
447                 logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
448             }
449         }
450         return done;
451     }
452
453     /**
454      * Send a message to set the alarm time and date using the system time and date
455      *
456      * @return true if the message was sent or false if not
457      */
458     public boolean sendSetTime() {
459         logger.debug("sendSetTime()");
460
461         boolean done = false;
462
463         if (autoSyncTime) {
464             GregorianCalendar cal = new GregorianCalendar();
465             if (cal.get(Calendar.YEAR) >= 2000) {
466                 logger.debug("sendSetTime(): sync time {}",
467                         String.format("%02d/%02d/%04d %02d:%02d:%02d", cal.get(Calendar.DAY_OF_MONTH),
468                                 cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR), cal.get(Calendar.HOUR_OF_DAY),
469                                 cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)));
470
471                 byte[] dynPart = new byte[6];
472                 dynPart[0] = (byte) cal.get(Calendar.SECOND);
473                 dynPart[1] = (byte) cal.get(Calendar.MINUTE);
474                 dynPart[2] = (byte) cal.get(Calendar.HOUR_OF_DAY);
475                 dynPart[3] = (byte) cal.get(Calendar.DAY_OF_MONTH);
476                 dynPart[4] = (byte) (cal.get(Calendar.MONTH) + 1);
477                 dynPart[5] = (byte) (cal.get(Calendar.YEAR) - 2000);
478
479                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.SETTIME, dynPart), false, 0);
480
481                 cal.set(Calendar.MILLISECOND, 0);
482                 syncTimeCheck = cal.getTimeInMillis();
483             } else {
484                 logger.info(
485                         "Powermax alarm binding: time not synchronized; please correct the date/time of your openHAB server");
486                 syncTimeCheck = null;
487             }
488         } else {
489             syncTimeCheck = null;
490         }
491         return done;
492     }
493
494     /**
495      * Send a message to the Powermax alarm panel to get all the event logs
496      *
497      * @param pinCode the PIN code. A string of 4 characters is expected
498      *
499      * @return true if the message was sent or false if not
500      */
501     public boolean requestEventLog(String pinCode) {
502         logger.debug("requestEventLog()");
503
504         boolean done = false;
505
506         if ((pinCode == null) || (pinCode.length() != 4)) {
507             logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
508         } else {
509             try {
510                 byte[] dynPart = new byte[3];
511                 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
512                 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
513
514                 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.EVENTLOG, dynPart), false, 0, true);
515             } catch (NumberFormatException e) {
516                 logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
517             }
518         }
519         return done;
520     }
521
522     /**
523      * Start downloading panel setup
524      *
525      * @return true if the message was sent or the sending is delayed; false in other cases
526      */
527     public synchronized boolean startDownload() {
528         if (downloadRunning) {
529             return false;
530         } else {
531             lastTimeDownloadRequested = System.currentTimeMillis();
532             downloadRunning = true;
533             return sendMessage(PowermaxSendType.DOWNLOAD);
534         }
535     }
536
537     /**
538      * Act the exit of the panel setup
539      */
540     public synchronized void exitDownload() {
541         downloadRunning = false;
542     }
543
544     public void retryDownloadSetup(int remainingAttempts) {
545         long now = System.currentTimeMillis();
546         if ((remainingAttempts > 0) && !isDownloadRunning() && ((lastTimeDownloadRequested == null)
547                 || ((now - lastTimeDownloadRequested) >= DELAY_BETWEEN_SETUP_DOWNLOADS))) {
548             // We wait at least 45 seconds before each retry to download the panel setup
549             logger.debug("Powermax alarm binding: try again downloading setup");
550             startDownload();
551         }
552     }
553
554     public void getInfosWhenInStandardMode() {
555         sendMessage(PowermaxSendType.ZONESNAME);
556         sendMessage(PowermaxSendType.ZONESTYPE);
557         sendMessage(PowermaxSendType.STATUS);
558     }
559
560     public void sendRestoreMessage() {
561         sendMessage(PowermaxSendType.RESTORE);
562     }
563
564     /**
565      * @return true if a download of the panel setup is in progress
566      */
567     public boolean isDownloadRunning() {
568         return downloadRunning;
569     }
570
571     /**
572      * @return the time in milliseconds the last download of the panel setup was requested or null if not yet requested
573      */
574     public Long getLastTimeDownloadRequested() {
575         return lastTimeDownloadRequested;
576     }
577
578     /**
579      * Send a ENROLL message
580      *
581      * @return true if the message was sent or the sending is delayed; false in other cases
582      */
583     public boolean enrollPowerlink() {
584         return sendMessage(new PowermaxBaseMessage(PowermaxSendType.ENROLL), true, 0);
585     }
586
587     /**
588      * Send a message or delay the sending if time frame for receiving response is not ended
589      *
590      * @param msgType the message type to be sent
591      *
592      * @return true if the message was sent or the sending is delayed; false in other cases
593      */
594     public boolean sendMessage(PowermaxSendType msgType) {
595         return sendMessage(new PowermaxBaseMessage(msgType), false, 0);
596     }
597
598     /**
599      * Delay the sending of a message
600      *
601      * @param msgType the message type to be sent
602      * @param waitTime the delay in seconds to wait
603      *
604      * @return true if the sending is delayed; false in other cases
605      */
606     public boolean sendMessageLater(PowermaxSendType msgType, int waitTime) {
607         return sendMessage(new PowermaxBaseMessage(msgType), false, waitTime);
608     }
609
610     private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime) {
611         return sendMessage(msg, immediate, waitTime, false);
612     }
613
614     /**
615      * Send a message or delay the sending if time frame for receiving response is not ended
616      *
617      * @param msg the message to be sent
618      * @param immediate true if the message has to be send without considering timing
619      * @param waitTime the delay in seconds to wait
620      * @param doNotLog true if the message contains data that must not be logged
621      *
622      * @return true if the message was sent or the sending is delayed; false in other cases
623      */
624     @SuppressWarnings("PMD.CompareObjectsWithEquals")
625     private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime,
626             boolean doNotLog) {
627         if ((waitTime > 0) && (msg != null)) {
628             logger.debug("sendMessage(): delay ({} s) sending message (type {})", waitTime, msg.getSendType());
629             // Don't queue the message
630             PowermaxBaseMessage msgToSendLater = new PowermaxBaseMessage(msg.getRawData());
631             msgToSendLater.setSendType(msg.getSendType());
632             scheduler.schedule(() -> {
633                 sendMessage(msgToSendLater, false, 0);
634             }, waitTime, TimeUnit.SECONDS);
635             return true;
636         }
637
638         if (msg == null) {
639             msg = msgQueue.peek();
640             if (msg == null) {
641                 logger.debug("sendMessage(): nothing to send");
642                 return false;
643             }
644         }
645
646         // Delay sending if time frame for receiving response is not ended
647         long delay = WAITING_DELAY_FOR_RESPONSE - (System.currentTimeMillis() - connector.getWaitingForResponse());
648
649         PowermaxBaseMessage msgToSend = msg;
650
651         if (!immediate) {
652             msgToSend = msgQueue.peek();
653             if (msgToSend != msg) {
654                 logger.debug("sendMessage(): add message in queue (type {})", msg.getSendType());
655                 msgQueue.offer(msg);
656                 msgToSend = msgQueue.peek();
657             }
658             if ((msgToSend != msg) && (delay > 0)) {
659                 return true;
660             } else if ((msgToSend == msg) && (delay > 0)) {
661                 if (delay < 100) {
662                     delay = 100;
663                 }
664                 logger.debug("sendMessage(): delay ({} ms) sending message (type {})", delay, msgToSend.getSendType());
665                 scheduler.schedule(() -> {
666                     sendMessage(null, false, 0);
667                 }, delay, TimeUnit.MILLISECONDS);
668                 return true;
669             } else {
670                 msgToSend = msgQueue.poll();
671             }
672         }
673
674         if (logger.isDebugEnabled()) {
675             logger.debug("sendMessage(): sending {} message {}", msgToSend.getSendType(),
676                     doNotLog ? "***" : HexUtils.bytesToHex(msgToSend.getRawData()));
677         }
678         boolean done = sendMessage(msgToSend.getRawData());
679         if (done) {
680             lastSendMsg = msgToSend;
681             connector.setWaitingForResponse(System.currentTimeMillis());
682
683             if (!immediate && (msgQueue.peek() != null)) {
684                 logger.debug("sendMessage(): delay sending next message (type {})", msgQueue.peek().getSendType());
685                 scheduler.schedule(() -> {
686                     sendMessage(null, false, 0);
687                 }, WAITING_DELAY_FOR_RESPONSE, TimeUnit.MILLISECONDS);
688             }
689         } else {
690             logger.debug("sendMessage(): failed");
691         }
692
693         return done;
694     }
695
696     /**
697      * Send a message to the Powermax alarm panel
698      *
699      * @param data the data buffer containing the message to be sent
700      *
701      * @return true if the message was sent or false if not
702      */
703     private boolean sendMessage(byte[] data) {
704         boolean done = false;
705         if (isConnected()) {
706             data[data.length - 2] = computeCRC(data, data.length);
707             connector.sendMessage(data);
708             done = connector.isConnected();
709         } else {
710             logger.debug("sendMessage(): aborted (not connected)");
711         }
712         return done;
713     }
714 }