2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.powermax.internal.message;
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;
22 import java.util.concurrent.ConcurrentLinkedQueue;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
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;
46 * A class that manages the communication with the Visonic alarm system
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
52 * @author Laurent Garnier - Initial contribution
55 public class PowermaxCommManager implements PowermaxMessageEventListener {
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);
63 private final Logger logger = LoggerFactory.getLogger(PowermaxCommManager.class);
65 private final ScheduledExecutorService scheduler;
67 private final TimeZoneProvider timeZoneProvider;
69 /** The object to store the current settings of the Powermax alarm system */
70 private final PowermaxPanelSettings panelSettings;
72 /** Panel type used when in standard mode */
73 private final PowermaxPanelType panelType;
75 private final boolean forceStandardMode;
76 private final boolean autoSyncTime;
78 private final List<PowermaxStateEventListener> listeners = new ArrayList<>();
80 /** The serial or TCP connecter used to communicate with the Powermax alarm system */
81 private final PowermaxConnector connector;
83 /** The last message sent to the the Powermax alarm system */
84 private @Nullable PowermaxBaseMessage lastSendMsg;
86 /** The message queue of messages to be sent to the the Powermax alarm system */
87 private ConcurrentLinkedQueue<PowermaxBaseMessage> msgQueue = new ConcurrentLinkedQueue<>();
89 /** The time in milliseconds the last download of the panel setup was requested */
90 private long lastTimeDownloadRequested;
92 /** The boolean indicating if the download of the panel setup is in progress or not */
93 private boolean downloadRunning;
95 /** The time in milliseconds used to set time and date */
96 private long syncTimeCheck;
99 * Constructor for Serial Connection
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
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");
122 * Constructor for TCP connection
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
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");
147 * @param listener the listener to be added
149 public synchronized void addEventListener(PowermaxStateEventListener listener) {
150 listeners.add(listener);
151 connector.addEventListener(this);
155 * Remove event listener
157 * @param listener the listener to be removed
159 public synchronized void removeEventListener(PowermaxStateEventListener listener) {
160 connector.removeEventListener(this);
161 listeners.remove(listener);
165 * Connect to the Powermax alarm system
167 public void open() throws Exception {
170 msgQueue = new ConcurrentLinkedQueue<>();
174 * Close the connection to the Powermax alarm system.
176 * @return true if connected or false if not
178 public boolean close() {
180 lastTimeDownloadRequested = 0;
181 downloadRunning = false;
182 return isConnected();
186 * @return true if connected to the Powermax alarm system or false if not
188 public boolean isConnected() {
189 return connector.isConnected();
193 * @return the current settings of the Powermax alarm system
195 public PowermaxPanelSettings getPanelSettings() {
196 return panelSettings;
200 * Process and store all the panel settings from the raw buffers
202 * @param powerlinkMode true if in Powerlink mode or false if in standard mode
204 * @return true if no problem encountered to get all the settings; false if not
206 public boolean processPanelSettings(boolean powerlinkMode) {
207 return panelSettings.process(powerlinkMode, panelType, powerlinkMode ? syncTimeCheck : 0);
211 * @return a new instance of PowermaxState
213 public PowermaxState createNewState() {
214 return new PowermaxState(panelSettings, timeZoneProvider);
218 * @return the last message sent to the Powermax alarm system
220 public synchronized @Nullable PowermaxBaseMessage getLastSendMsg() {
225 public void onNewMessageEvent(EventObject event) {
226 PowermaxMessageEvent messageEvent = (PowermaxMessageEvent) event;
227 PowermaxBaseMessage message = messageEvent.getMessage();
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()));
236 if (forceStandardMode && message instanceof PowermaxPowerlinkMessage) {
237 message = new PowermaxBaseMessage(message.getRawData());
240 PowermaxState updateState = message.handleMessage(this);
242 if (updateState == null) {
243 updateState = createNewState();
246 updateState.lastMessageTime.setValue(System.currentTimeMillis());
248 byte[] buffer = updateState.getUpdateSettings();
249 if (buffer != null) {
250 panelSettings.updateRawSettings(buffer);
252 if (!updateState.getUpdatedZoneNames().isEmpty()) {
253 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
254 panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx));
257 if (!updateState.getUpdatedZoneInfos().isEmpty()) {
258 for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) {
259 panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx));
263 PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState);
265 // send message to event listeners
266 listeners.forEach(listener -> listener.onNewStateEvent(newEvent));
270 public void onCommunicationFailure(String message) {
272 listeners.forEach(listener -> listener.onCommunicationFailure(message));
276 * Compute the CRC of a message
278 * @param data the buffer containing the message
279 * @param len the size of the message in the buffer
281 * @return the computed CRC
283 public static byte computeCRC(byte[] data, int len) {
285 for (int i = 1; i < (len - 2); i++) {
286 checksum = checksum + (data[i] & 0x000000FF);
288 checksum = 0xFF - (checksum % 0xFF);
289 if (checksum == 0xFF) {
292 return (byte) checksum;
296 * Send an ACK for a received message
298 * @param msg the received message object
299 * @param ackType the type of ACK to be sent
301 * @return true if the ACK was sent or false if not
303 public synchronized boolean sendAck(PowermaxBaseMessage msg, byte ackType) {
304 int code = msg.getCode();
305 byte[] rawData = msg.getRawData();
307 if ((code >= 0x80) || ((code < 0x10) && (rawData[rawData.length - 3] == 0x43))) {
308 ackData = new byte[] { 0x0D, ackType, 0x43, 0x00, 0x0A };
310 ackData = new byte[] { 0x0D, ackType, 0x00, 0x0A };
313 if (logger.isDebugEnabled()) {
314 logger.debug("sendAck(): sending message {}", HexUtils.bytesToHex(ackData));
316 boolean done = sendMessage(ackData);
318 logger.debug("sendAck(): failed");
324 * Send a message to the Powermax alarm panel to change arm mode
326 * @param armMode the arm mode
327 * @param pinCode the PIN code. A string of 4 characters is expected
329 * @return true if the message was sent or false if not
331 public boolean requestArmMode(PowermaxArmMode armMode, String pinCode) {
332 logger.debug("requestArmMode(): armMode = {}", armMode.getShortName());
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());
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);
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());
357 * Send a message to the Powermax alarm panel to change PGM or X10 zone state
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
362 * @return true if the message was sent or false if not
364 public boolean sendPGMX10(Command action, @Nullable Byte device) {
365 logger.debug("sendPGMX10(): action = {}, device = {}", action, device);
367 boolean done = false;
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);
375 Byte code = codes.get(action.toString());
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);
381 int val = (device == null) ? 1 : (1 << device);
382 byte[] dynPart = new byte[3];
384 dynPart[1] = (byte) (val & 0x000000FF);
385 dynPart[2] = (byte) (val >> 8);
387 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.X10PGM, dynPart), false, 0);
393 * Send a message to the Powermax alarm panel to bypass a zone or to not bypass a zone
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
399 * @return true if the message was sent or false if not
401 public boolean sendZoneBypass(boolean bypass, byte zone, String pinCode) {
402 logger.debug("sendZoneBypass(): bypass = {}, zone = {}", bypass ? "true" : "false", zone);
404 boolean done = false;
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);
412 int val = (1 << (zone - 1));
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);
418 for (i = 2; i < 10; i++) {
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);
427 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASS, dynPart), false, 0, true);
429 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASSTAT), false, 0);
431 } catch (NumberFormatException e) {
432 logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
439 * Send a message to set the alarm time and date using the system time and date
441 * @return true if the message was sent or false if not
443 public boolean sendSetTime() {
444 logger.debug("sendSetTime()");
446 boolean done = false;
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)));
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);
464 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.SETTIME, dynPart), false, 0);
466 cal.set(Calendar.MILLISECOND, 0);
467 syncTimeCheck = cal.getTimeInMillis();
470 "Powermax alarm binding: time not synchronized; please correct the date/time of your openHAB server");
480 * Send a message to the Powermax alarm panel to get all the event logs
482 * @param pinCode the PIN code. A string of 4 characters is expected
484 * @return true if the message was sent or false if not
486 public boolean requestEventLog(String pinCode) {
487 logger.debug("requestEventLog()");
489 boolean done = false;
491 if (pinCode.length() != 4) {
492 logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
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);
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");
508 * Start downloading panel setup
510 * @return true if the message was sent or the sending is delayed; false in other cases
512 public synchronized boolean startDownload() {
513 if (downloadRunning) {
516 lastTimeDownloadRequested = System.currentTimeMillis();
517 downloadRunning = true;
518 return sendMessage(PowermaxSendType.DOWNLOAD);
523 * Act the exit of the panel setup
525 public synchronized void exitDownload() {
526 downloadRunning = false;
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");
539 public void getInfosWhenInStandardMode() {
540 sendMessage(PowermaxSendType.ZONESNAME);
541 sendMessage(PowermaxSendType.ZONESTYPE);
542 sendMessage(PowermaxSendType.STATUS);
545 public void sendRestoreMessage() {
546 sendMessage(PowermaxSendType.RESTORE);
550 * @return true if a download of the panel setup is in progress
552 public boolean isDownloadRunning() {
553 return downloadRunning;
557 * @return the time in milliseconds the last download of the panel setup was requested or 0 if not yet requested
559 public long getLastTimeDownloadRequested() {
560 return lastTimeDownloadRequested;
564 * Send an ENROLL message
566 * @return true if the message was sent or the sending is delayed; false in other cases
568 public boolean enrollPowerlink() {
569 return sendMessage(new PowermaxBaseMessage(PowermaxSendType.ENROLL), true, 0);
573 * Send a message or delay the sending if time frame for receiving response is not ended
575 * @param msgType the message type to be sent
577 * @return true if the message was sent or the sending is delayed; false in other cases
579 public boolean sendMessage(PowermaxSendType msgType) {
580 return sendMessage(new PowermaxBaseMessage(msgType), false, 0);
584 * Delay the sending of a message
586 * @param msgType the message type to be sent
587 * @param waitTime the delay in seconds to wait
589 * @return true if the sending is delayed; false in other cases
591 public boolean sendMessageLater(PowermaxSendType msgType, int waitTime) {
592 return sendMessage(new PowermaxBaseMessage(msgType), false, waitTime);
595 private synchronized boolean sendMessage(@Nullable PowermaxBaseMessage msg, boolean immediate, int waitTime) {
596 return sendMessage(msg, immediate, waitTime, false);
600 * Send a message or delay the sending if time frame for receiving response is not ended
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
607 * @return true if the message was sent or the sending is delayed; false in other cases
609 @SuppressWarnings("PMD.CompareObjectsWithEquals")
610 private synchronized boolean sendMessage(@Nullable PowermaxBaseMessage msg, boolean immediate, int waitTime,
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);
624 msg = msgQueue.peek();
626 logger.debug("sendMessage(): nothing to send");
631 // Delay sending if time frame for receiving response is not ended
632 long delay = WAITING_DELAY_FOR_RESPONSE - (System.currentTimeMillis() - connector.getWaitingForResponse());
634 PowermaxBaseMessage msgToSend = msg;
637 msgToSend = msgQueue.peek();
638 if (msgToSend != msg) {
639 logger.debug("sendMessage(): add message in queue (type {})", msg.getSendType());
641 msgToSend = msgQueue.peek();
643 if ((msgToSend != msg) && (delay > 0)) {
645 } else if ((msgToSend == msg) && (delay > 0)) {
649 logger.debug("sendMessage(): delay ({} ms) sending message (type {})", delay, msgToSend.getSendType());
650 scheduler.schedule(() -> {
651 sendMessage(null, false, 0);
652 }, delay, TimeUnit.MILLISECONDS);
655 msgToSend = msgQueue.poll();
659 if (logger.isDebugEnabled()) {
660 logger.debug("sendMessage(): sending {} message {}", msgToSend.getSendType(),
661 doNotLog ? "***" : HexUtils.bytesToHex(msgToSend.getRawData()));
663 boolean done = sendMessage(msgToSend.getRawData());
665 lastSendMsg = msgToSend;
666 connector.setWaitingForResponse(System.currentTimeMillis());
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);
675 logger.debug("sendMessage(): failed");
682 * Send a message to the Powermax alarm panel
684 * @param data the data buffer containing the message to be sent
686 * @return true if the message was sent or false if not
688 private boolean sendMessage(byte[] data) {
689 boolean done = false;
691 data[data.length - 2] = computeCRC(data, data.length);
692 connector.sendMessage(data);
693 done = connector.isConnected();
695 logger.debug("sendMessage(): aborted (not connected)");