2 * Copyright (c) 2010-2021 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.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;
44 * A class that manages the communication with the Visonic alarm system
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
50 * @author Laurent Garnier - Initial contribution
52 public class PowermaxCommManager implements PowermaxMessageEventListener {
54 private final Logger logger = LoggerFactory.getLogger(PowermaxCommManager.class);
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);
62 private final ScheduledExecutorService scheduler;
64 /** The object to store the current settings of the Powermax alarm system */
65 private PowermaxPanelSettings panelSettings;
67 /** Panel type used when in standard mode */
68 private PowermaxPanelType panelType;
70 private boolean forceStandardMode;
71 private boolean autoSyncTime;
72 private final TimeZoneProvider timeZoneProvider;
74 private List<PowermaxStateEventListener> listeners = new ArrayList<>();
76 /** The serial or TCP connecter used to communicate with the Powermax alarm system */
77 private PowermaxConnector connector;
79 /** The last message sent to the the Powermax alarm system */
80 private PowermaxBaseMessage lastSendMsg;
82 /** The message queue of messages to be sent to the the Powermax alarm system */
83 private ConcurrentLinkedQueue<PowermaxBaseMessage> msgQueue = new ConcurrentLinkedQueue<>();
85 /** The time in milliseconds the last download of the panel setup was requested */
86 private Long lastTimeDownloadRequested;
88 /** The boolean indicating if the download of the panel setup is in progress or not */
89 private boolean downloadRunning;
91 /** The time in milliseconds used to set time and date */
92 private Long syncTimeCheck;
95 * Constructor for Serial Connection
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
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");
123 * Constructor for TCP connection
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
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");
153 * @param listener the listener to be added
155 public synchronized void addEventListener(PowermaxStateEventListener listener) {
156 listeners.add(listener);
157 if (connector != null) {
158 connector.addEventListener(this);
163 * Remove event listener
165 * @param listener the listener to be removed
167 public synchronized void removeEventListener(PowermaxStateEventListener listener) {
168 if (connector != null) {
169 connector.removeEventListener(this);
171 listeners.remove(listener);
175 * Connect to the Powermax alarm system
177 * @return true if connected or false if not
179 public boolean open() {
180 if (connector != null) {
184 msgQueue = new ConcurrentLinkedQueue<>();
185 return isConnected();
189 * Close the connection to the Powermax alarm system.
191 * @return true if connected or false if not
193 public boolean close() {
194 if (connector != null) {
197 lastTimeDownloadRequested = null;
198 downloadRunning = false;
199 return isConnected();
203 * @return true if connected to the Powermax alarm system or false if not
205 public boolean isConnected() {
206 return (connector != null) && connector.isConnected();
210 * @return the current settings of the Powermax alarm system
212 public PowermaxPanelSettings getPanelSettings() {
213 return panelSettings;
217 * Process and store all the panel settings from the raw buffers
219 * @param PowerlinkMode true if in Powerlink mode or false if in standard mode
221 * @return true if no problem encountered to get all the settings; false if not
223 public boolean processPanelSettings(boolean powerlinkMode) {
224 return panelSettings.process(powerlinkMode, panelType, powerlinkMode ? syncTimeCheck : null);
228 * @return a new instance of PowermaxState
230 public PowermaxState createNewState() {
231 return new PowermaxState(panelSettings, timeZoneProvider);
235 * @return the last message sent to the Powermax alarm system
237 public synchronized PowermaxBaseMessage getLastSendMsg() {
242 public void onNewMessageEvent(EventObject event) {
243 PowermaxMessageEvent messageEvent = (PowermaxMessageEvent) event;
244 PowermaxBaseMessage message = messageEvent.getMessage();
246 if (logger.isDebugEnabled()) {
247 logger.debug("onNewMessageReceived(): received message 0x{} ({})",
248 HexUtils.bytesToHex(message.getRawData()),
249 (message.getReceiveType() != null) ? message.getReceiveType()
250 : String.format("%02X", message.getCode()));
253 if (forceStandardMode && message instanceof PowermaxPowerlinkMessage) {
254 message = new PowermaxBaseMessage(message.getRawData());
257 PowermaxState updateState = message.handleMessage(this);
258 if (updateState != null) {
259 if (updateState.getUpdateSettings() != null) {
260 panelSettings.updateRawSettings(updateState.getUpdateSettings());
262 if (!updateState.getUpdatedZoneNames().isEmpty()) {
263 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
264 panelSettings.updateZoneName(zoneIdx, updateState.getUpdatedZoneNames().get(zoneIdx));
267 if (!updateState.getUpdatedZoneInfos().isEmpty()) {
268 for (Integer zoneIdx : updateState.getUpdatedZoneInfos().keySet()) {
269 panelSettings.updateZoneInfo(zoneIdx, updateState.getUpdatedZoneInfos().get(zoneIdx));
273 PowermaxStateEvent newEvent = new PowermaxStateEvent(this, updateState);
275 // send message to event listeners
276 for (int i = 0; i < listeners.size(); i++) {
277 listeners.get(i).onNewStateEvent(newEvent);
283 * Compute the CRC of a message
285 * @param data the buffer containing the message
286 * @param len the size of the message in the buffer
288 * @return the computed CRC
290 public static byte computeCRC(byte[] data, int len) {
292 for (int i = 1; i < (len - 2); i++) {
293 checksum = checksum + (data[i] & 0x000000FF);
295 checksum = 0xFF - (checksum % 0xFF);
296 if (checksum == 0xFF) {
299 return (byte) checksum;
303 * Send an ACK for a received message
305 * @param msg the received message object
306 * @param ackType the type of ACK to be sent
308 * @return true if the ACK was sent or false if not
310 public synchronized boolean sendAck(PowermaxBaseMessage msg, byte ackType) {
311 int code = msg.getCode();
312 byte[] rawData = msg.getRawData();
314 if ((code >= 0x80) || ((code < 0x10) && (rawData[rawData.length - 3] == 0x43))) {
315 ackData = new byte[] { 0x0D, ackType, 0x43, 0x00, 0x0A };
317 ackData = new byte[] { 0x0D, ackType, 0x00, 0x0A };
320 if (logger.isDebugEnabled()) {
321 logger.debug("sendAck(): sending message {}", HexUtils.bytesToHex(ackData));
323 boolean done = sendMessage(ackData);
325 logger.debug("sendAck(): failed");
331 * Send a message to the Powermax alarm panel to change arm mode
333 * @param armMode the arm mode
334 * @param pinCode the PIN code. A string of 4 characters is expected
336 * @return true if the message was sent or false if not
338 public boolean requestArmMode(PowermaxArmMode armMode, String pinCode) {
339 logger.debug("requestArmMode(): armMode = {}", armMode.getShortName());
341 boolean done = false;
342 if (!armMode.isAllowedCommand()) {
343 logger.debug("Powermax alarm binding: requested arm mode {} rejected", armMode.getShortName());
344 } else if ((pinCode == null) || (pinCode.length() != 4)) {
345 logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
346 armMode.getShortName());
349 byte[] dynPart = new byte[3];
350 dynPart[0] = armMode.getCommandCode();
351 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
352 dynPart[2] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
354 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.ARM, dynPart), false, 0, true);
355 } catch (NumberFormatException e) {
356 logger.debug("Powermax alarm binding: requested arm mode {} rejected due to invalid PIN code",
357 armMode.getShortName());
364 * Send a message to the Powermax alarm panel to change PGM or X10 zone state
366 * @param action the requested action. Allowed values are: OFF, ON, DIM, BRIGHT
367 * @param device the X10 device number. null is expected for PGM
369 * @return true if the message was sent or false if not
371 public boolean sendPGMX10(Command action, Byte device) {
372 logger.debug("sendPGMX10(): action = {}, device = {}", action, device);
374 boolean done = false;
376 Map<String, Byte> codes = new HashMap<>();
377 codes.put("OFF", (byte) 0x00);
378 codes.put("ON", (byte) 0x01);
379 codes.put("DIM", (byte) 0x0A);
380 codes.put("BRIGHT", (byte) 0x0B);
382 Byte code = codes.get(action.toString());
384 logger.debug("Powermax alarm binding: invalid PGM/X10 command: {}", action);
385 } else if ((device != null) && ((device < 1) || (device >= panelSettings.getNbPGMX10Devices()))) {
386 logger.debug("Powermax alarm binding: invalid X10 device id: {}", device);
388 int val = (device == null) ? 1 : (1 << device);
389 byte[] dynPart = new byte[3];
391 dynPart[1] = (byte) (val & 0x000000FF);
392 dynPart[2] = (byte) (val >> 8);
394 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.X10PGM, dynPart), false, 0);
400 * Send a message to the Powermax alarm panel to bypass a zone or to not bypass a zone
402 * @param bypass true to bypass the zone; false to not bypass the zone
403 * @param zone the zone number (first zone is number 1)
404 * @param pinCode the PIN code. A string of 4 characters is expected
406 * @return true if the message was sent or false if not
408 public boolean sendZoneBypass(boolean bypass, byte zone, String pinCode) {
409 logger.debug("sendZoneBypass(): bypass = {}, zone = {}", bypass ? "true" : "false", zone);
411 boolean done = false;
413 if ((pinCode == null) || (pinCode.length() != 4)) {
414 logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
415 } else if ((zone < 1) || (zone > panelSettings.getNbZones())) {
416 logger.debug("Powermax alarm binding: invalid zone number: {}", zone);
419 int val = (1 << (zone - 1));
421 byte[] dynPart = new byte[10];
422 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
423 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
425 for (i = 2; i < 10; i++) {
429 dynPart[i++] = (byte) (val & 0x000000FF);
430 dynPart[i++] = (byte) ((val >> 8) & 0x000000FF);
431 dynPart[i++] = (byte) ((val >> 16) & 0x000000FF);
432 dynPart[i++] = (byte) ((val >> 24) & 0x000000FF);
434 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASS, dynPart), false, 0, true);
436 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.BYPASSTAT), false, 0);
438 } catch (NumberFormatException e) {
439 logger.debug("Powermax alarm binding: zone bypass rejected due to invalid PIN code");
446 * Send a message to set the alarm time and date using the system time and date
448 * @return true if the message was sent or false if not
450 public boolean sendSetTime() {
451 logger.debug("sendSetTime()");
453 boolean done = false;
456 GregorianCalendar cal = new GregorianCalendar();
457 if (cal.get(Calendar.YEAR) >= 2000) {
458 logger.debug("sendSetTime(): sync time {}",
459 String.format("%02d/%02d/%04d %02d:%02d:%02d", cal.get(Calendar.DAY_OF_MONTH),
460 cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR), cal.get(Calendar.HOUR_OF_DAY),
461 cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)));
463 byte[] dynPart = new byte[6];
464 dynPart[0] = (byte) cal.get(Calendar.SECOND);
465 dynPart[1] = (byte) cal.get(Calendar.MINUTE);
466 dynPart[2] = (byte) cal.get(Calendar.HOUR_OF_DAY);
467 dynPart[3] = (byte) cal.get(Calendar.DAY_OF_MONTH);
468 dynPart[4] = (byte) (cal.get(Calendar.MONTH) + 1);
469 dynPart[5] = (byte) (cal.get(Calendar.YEAR) - 2000);
471 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.SETTIME, dynPart), false, 0);
473 cal.set(Calendar.MILLISECOND, 0);
474 syncTimeCheck = cal.getTimeInMillis();
477 "Powermax alarm binding: time not synchronized; please correct the date/time of your openHAB server");
478 syncTimeCheck = null;
481 syncTimeCheck = null;
487 * Send a message to the Powermax alarm panel to get all the event logs
489 * @param pinCode the PIN code. A string of 4 characters is expected
491 * @return true if the message was sent or false if not
493 public boolean requestEventLog(String pinCode) {
494 logger.debug("requestEventLog()");
496 boolean done = false;
498 if ((pinCode == null) || (pinCode.length() != 4)) {
499 logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
502 byte[] dynPart = new byte[3];
503 dynPart[0] = (byte) Integer.parseInt(pinCode.substring(0, 2), 16);
504 dynPart[1] = (byte) Integer.parseInt(pinCode.substring(2, 4), 16);
506 done = sendMessage(new PowermaxBaseMessage(PowermaxSendType.EVENTLOG, dynPart), false, 0, true);
507 } catch (NumberFormatException e) {
508 logger.debug("Powermax alarm binding: requested event log rejected due to invalid PIN code");
515 * Start downloading panel setup
517 * @return true if the message was sent or the sending is delayed; false in other cases
519 public synchronized boolean startDownload() {
520 if (downloadRunning) {
523 lastTimeDownloadRequested = System.currentTimeMillis();
524 downloadRunning = true;
525 return sendMessage(PowermaxSendType.DOWNLOAD);
530 * Act the exit of the panel setup
532 public synchronized void exitDownload() {
533 downloadRunning = false;
536 public void retryDownloadSetup(int remainingAttempts) {
537 long now = System.currentTimeMillis();
538 if ((remainingAttempts > 0) && !isDownloadRunning() && ((lastTimeDownloadRequested == null)
539 || ((now - lastTimeDownloadRequested) >= DELAY_BETWEEN_SETUP_DOWNLOADS))) {
540 // We wait at least 45 seconds before each retry to download the panel setup
541 logger.debug("Powermax alarm binding: try again downloading setup");
546 public void getInfosWhenInStandardMode() {
547 sendMessage(PowermaxSendType.ZONESNAME);
548 sendMessage(PowermaxSendType.ZONESTYPE);
549 sendMessage(PowermaxSendType.STATUS);
552 public void sendRestoreMessage() {
553 sendMessage(PowermaxSendType.RESTORE);
557 * @return true if a download of the panel setup is in progress
559 public boolean isDownloadRunning() {
560 return downloadRunning;
564 * @return the time in milliseconds the last download of the panel setup was requested or null if not yet requested
566 public Long getLastTimeDownloadRequested() {
567 return lastTimeDownloadRequested;
571 * Send a ENROLL message
573 * @return true if the message was sent or the sending is delayed; false in other cases
575 public boolean enrollPowerlink() {
576 return sendMessage(new PowermaxBaseMessage(PowermaxSendType.ENROLL), true, 0);
580 * Send a message or delay the sending if time frame for receiving response is not ended
582 * @param msgType the message type to be sent
584 * @return true if the message was sent or the sending is delayed; false in other cases
586 public boolean sendMessage(PowermaxSendType msgType) {
587 return sendMessage(new PowermaxBaseMessage(msgType), false, 0);
591 * Delay the sending of a message
593 * @param msgType the message type to be sent
594 * @param waitTime the delay in seconds to wait
596 * @return true if the sending is delayed; false in other cases
598 public boolean sendMessageLater(PowermaxSendType msgType, int waitTime) {
599 return sendMessage(new PowermaxBaseMessage(msgType), false, waitTime);
602 private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime) {
603 return sendMessage(msg, immediate, waitTime, false);
607 * Send a message or delay the sending if time frame for receiving response is not ended
609 * @param msg the message to be sent
610 * @param immediate true if the message has to be send without considering timing
611 * @param waitTime the delay in seconds to wait
612 * @param doNotLog true if the message contains data that must not be logged
614 * @return true if the message was sent or the sending is delayed; false in other cases
616 private synchronized boolean sendMessage(PowermaxBaseMessage msg, boolean immediate, int waitTime,
618 if ((waitTime > 0) && (msg != null)) {
619 logger.debug("sendMessage(): delay ({} s) sending message (type {})", waitTime, msg.getSendType());
620 // Don't queue the message
621 PowermaxBaseMessage msgToSendLater = new PowermaxBaseMessage(msg.getRawData());
622 msgToSendLater.setSendType(msg.getSendType());
623 scheduler.schedule(() -> {
624 sendMessage(msgToSendLater, false, 0);
625 }, waitTime, TimeUnit.SECONDS);
630 msg = msgQueue.peek();
632 logger.debug("sendMessage(): nothing to send");
637 // Delay sending if time frame for receiving response is not ended
638 long delay = WAITING_DELAY_FOR_RESPONSE - (System.currentTimeMillis() - connector.getWaitingForResponse());
640 PowermaxBaseMessage msgToSend = msg;
643 msgToSend = msgQueue.peek();
644 if (msgToSend != msg) {
645 logger.debug("sendMessage(): add message in queue (type {})", msg.getSendType());
647 msgToSend = msgQueue.peek();
649 if ((msgToSend != msg) && (delay > 0)) {
651 } else if ((msgToSend == msg) && (delay > 0)) {
655 logger.debug("sendMessage(): delay ({} ms) sending message (type {})", delay, msgToSend.getSendType());
656 scheduler.schedule(() -> {
657 sendMessage(null, false, 0);
658 }, delay, TimeUnit.MILLISECONDS);
661 msgToSend = msgQueue.poll();
665 if (logger.isDebugEnabled()) {
666 logger.debug("sendMessage(): sending {} message {}", msgToSend.getSendType(),
667 doNotLog ? "***" : HexUtils.bytesToHex(msgToSend.getRawData()));
669 boolean done = sendMessage(msgToSend.getRawData());
671 lastSendMsg = msgToSend;
672 connector.setWaitingForResponse(System.currentTimeMillis());
674 if (!immediate && (msgQueue.peek() != null)) {
675 logger.debug("sendMessage(): delay sending next message (type {})", msgQueue.peek().getSendType());
676 scheduler.schedule(() -> {
677 sendMessage(null, false, 0);
678 }, WAITING_DELAY_FOR_RESPONSE, TimeUnit.MILLISECONDS);
681 logger.debug("sendMessage(): failed");
688 * Send a message to the Powermax alarm panel
690 * @param data the data buffer containing the message to be sent
692 * @return true if the message was sent or false if not
694 private boolean sendMessage(byte[] data) {
695 boolean done = false;
697 data[data.length - 2] = computeCRC(data, data.length);
698 connector.sendMessage(data);
699 done = connector.isConnected();
701 logger.debug("sendMessage(): aborted (not connected)");