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