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.state;
15 import java.io.UnsupportedEncodingException;
16 import java.util.Arrays;
17 import java.util.Calendar;
18 import java.util.GregorianCalendar;
20 import org.openhab.binding.powermax.internal.message.PowermaxSendType;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
25 * A class to store all the settings of the alarm system
27 * @author Laurent Garnier - Initial contribution
29 public class PowermaxPanelSettings {
31 private final Logger logger = LoggerFactory.getLogger(PowermaxPanelSettings.class);
33 /** Number of PGM and X10 devices managed by the system */
34 private static final int NB_PGM_X10_DEVICES = 16;
36 /** Raw buffers for settings */
37 private Byte[][] rawSettings;
39 private PowermaxPanelType panelType;
40 private String[] phoneNumbers;
42 private boolean silentPanic;
43 private boolean quickArm;
44 private boolean bypassEnabled;
45 private boolean partitionsEnabled;
46 private String[] pinCodes;
47 private String panelEprom;
48 private String panelSoftware;
49 private String panelSerial;
50 private PowermaxZoneSettings[] zoneSettings;
51 private PowermaxX10Settings[] x10Settings;
52 private boolean[] keypad1wEnrolled;
53 private boolean[] keypad2wEnrolled;
54 private boolean[] sirensEnrolled;
59 * @param defaultPanelType the default panel type to consider
61 public PowermaxPanelSettings(PowermaxPanelType defaultPanelType) {
62 rawSettings = new Byte[0x100][];
63 panelType = defaultPanelType;
64 phoneNumbers = new String[4];
66 int zoneCnt = panelType.getWireless() + panelType.getWired();
67 zoneSettings = new PowermaxZoneSettings[zoneCnt];
68 x10Settings = new PowermaxX10Settings[NB_PGM_X10_DEVICES];
72 * @return the panel type
74 public PowermaxPanelType getPanelType() {
79 * @return true if bypassing zones is enabled; false if not
81 public boolean isBypassEnabled() {
86 * @return true if partitions usage is enabled; false if not
88 public boolean isPartitionsEnabled() {
89 return partitionsEnabled;
93 * @return the panel EEPROM version
95 public String getPanelEprom() {
100 * @return the panel software version
102 public String getPanelSoftware() {
103 return panelSoftware;
107 * @return the panel serial ID
109 public String getPanelSerial() {
114 * @return the number of zones
116 public int getNbZones() {
117 return zoneSettings.length;
121 * Get the settings relative to a zone
123 * @param zone the zone index (from 1 to NumberOfZones)
125 * @return the settings of the zone
127 public PowermaxZoneSettings getZoneSettings(int zone) {
128 return ((zone < 1) || (zone > zoneSettings.length)) ? null : zoneSettings[zone - 1];
132 * @return the number of PGM and X10 devices managed by the system
134 public int getNbPGMX10Devices() {
135 return NB_PGM_X10_DEVICES;
139 * Get the settings relative to the PGM
141 * @return the settings of the PGM
143 public PowermaxX10Settings getPGMSettings() {
144 return x10Settings[0];
148 * Get the settings relative to a X10 device
150 * @param idx the index (from 1 to 15)
152 * @return the settings of the X10 device
154 public PowermaxX10Settings getX10Settings(int idx) {
155 return ((idx < 1) || (idx >= x10Settings.length)) ? null : x10Settings[idx];
159 * @param idx the keypad index (first is 1)
161 * @return true if the 1 way keypad is enrolled; false if not
163 public boolean isKeypad1wEnrolled(int idx) {
164 return ((keypad1wEnrolled == null) || (idx < 1) || (idx >= keypad1wEnrolled.length)) ? false
165 : keypad1wEnrolled[idx - 1];
169 * @param idx the keypad index (first is 1)
171 * @return true if the 2 way keypad is enrolled; false if not
173 public boolean isKeypad2wEnrolled(int idx) {
174 return ((keypad2wEnrolled == null) || (idx < 1) || (idx >= keypad2wEnrolled.length)) ? false
175 : keypad2wEnrolled[idx - 1];
179 * @param idx the siren index (first is 1)
181 * @return true if the siren is enrolled; false if not
183 public boolean isSirenEnrolled(int idx) {
184 return ((sirensEnrolled == null) || (idx < 1) || (idx >= sirensEnrolled.length)) ? false
185 : sirensEnrolled[idx - 1];
189 * @return the PIN code of the first user of null if unknown (standard mode)
191 public String getFirstPinCode() {
192 return (pinCodes == null) ? null : pinCodes[0];
195 public void updateRawSettings(byte[] data) {
196 if ((data == null) || (data.length < 3)) {
200 int end = data.length - 3;
201 int index = data[0] & 0x000000FF;
202 int page = data[1] & 0x000000FF;
203 int pageMin = page + (index + start) / 0x100;
204 int indexPageMin = (index + start) % 0x100;
205 int pageMax = page + (index + end) / 0x100;
206 int indexPageMax = (index + end) % 0x100;
208 for (int i = pageMin; i <= pageMax; i++) {
212 start = indexPageMin;
217 if (rawSettings[i] == null) {
218 rawSettings[i] = new Byte[0x100];
220 for (int j = start; j <= end; j++) {
221 rawSettings[i][j] = data[index++];
226 private byte[] readSettings(PowermaxSendType msgType, int start, int end) {
227 byte[] message = msgType.getMessage();
228 int page = message[2] & 0x000000FF;
229 int index = message[1] & 0x000000FF;
230 return readSettings(page, index + start, index + end);
233 private byte[] readSettings(int page, int start, int end) {
234 int pageMin = page + start / 0x100;
235 int indexPageMin = start % 0x100;
236 int pageMax = page + end / 0x100;
237 int indexPageMax = end % 0x100;
239 boolean missingData = false;
240 for (int i = pageMin; i <= pageMax; i++) {
244 start2 = indexPageMin;
249 index += end2 - start2 + 1;
250 for (int j = start2; j <= end2; j++) {
251 if ((rawSettings[i] == null) || (rawSettings[i][j] == null)) {
261 logger.debug("readSettings({}, {}, {}): missing data", page, start, end);
264 byte[] result = new byte[index];
266 for (int i = pageMin; i <= pageMax; i++) {
270 start2 = indexPageMin;
275 for (int j = start2; j <= end2; j++) {
276 result[index++] = rawSettings[i][j];
282 private String readSettingsAsString(PowermaxSendType msgType, int start, int end) {
283 byte[] message = msgType.getMessage();
284 int page = message[2] & 0x000000FF;
285 int index = message[1] & 0x000000FF;
286 String result = null;
287 byte[] data = readSettings(page, index + start, index + end);
288 if ((data != null) && ((data[0] & 0x000000FF) != 0x000000FF)) {
290 for (int i = 0; i < data.length; i++) {
291 boolean endStr = false;
292 switch (data[i] & 0x000000FF) {
306 if ((data[i] & 0x000000FF) >= 0x20) {
308 result += new String(data, i, 1, "US-ASCII");
309 } catch (UnsupportedEncodingException e) {
310 logger.debug("Unhandled character code {}", data[i]);
313 logger.debug("Unhandled character code {}", data[i]);
321 result = result.trim();
327 * Process and store all the panel settings from the raw buffers
329 * @param PowerlinkMode true if in Powerlink mode or false if in standard mode
330 * @param defaultPanelType the default panel type to consider if not found in the raw buffers
331 * @param timeSet the time in milliseconds used to set time and date; null if no sync time requested
333 * @return true if no problem encountered to get all the settings; false if not
335 @SuppressWarnings("null")
336 public boolean process(boolean PowerlinkMode, PowermaxPanelType defaultPanelType, Long timeSet) {
337 logger.debug("Process settings Powerlink {}", PowerlinkMode);
339 boolean result = true;
343 // Identify panel type
344 panelType = defaultPanelType;
346 data = readSettings(PowermaxSendType.DL_SERIAL, 7, 7);
349 panelType = PowermaxPanelType.fromCode(data[0]);
350 } catch (IllegalArgumentException e) {
351 logger.debug("Powermax alarm binding: unknwon panel type for code {}", data[0] & 0x000000FF);
352 panelType = defaultPanelType;
355 logger.debug("Cannot get panel type");
360 int zoneCnt = panelType.getWireless() + panelType.getWired();
361 int customCnt = panelType.getCustomZones();
362 int userCnt = panelType.getUserCodes();
363 int partitionCnt = panelType.getPartitions();
364 int sirenCnt = panelType.getSirens();
365 int keypad1wCnt = panelType.getKeypads1w();
366 int keypad2wCnt = panelType.getKeypads2w();
368 phoneNumbers = new String[4];
372 bypassEnabled = false;
373 partitionsEnabled = false;
374 pinCodes = new String[userCnt];
376 panelSoftware = null;
378 zoneSettings = new PowermaxZoneSettings[zoneCnt];
379 x10Settings = new PowermaxX10Settings[NB_PGM_X10_DEVICES];
380 keypad1wEnrolled = new boolean[keypad1wCnt];
381 keypad2wEnrolled = new boolean[keypad2wCnt];
382 sirensEnrolled = new boolean[sirenCnt];
385 // Check time and date
386 data = readSettings(PowermaxSendType.DL_TIME, 0, 5);
388 GregorianCalendar cal = new GregorianCalendar();
389 cal.set(Calendar.MILLISECOND, 0);
390 cal.set(Calendar.SECOND, data[0] & 0x000000FF);
391 cal.set(Calendar.MINUTE, data[1] & 0x000000FF);
392 cal.set(Calendar.HOUR_OF_DAY, data[2] & 0x000000FF);
393 cal.set(Calendar.DAY_OF_MONTH, data[3] & 0x000000FF);
394 cal.set(Calendar.MONTH, (data[4] & 0x000000FF) - 1);
395 cal.set(Calendar.YEAR, (data[5] & 0x000000FF) + 2000);
396 long timeRead = cal.getTimeInMillis();
397 logger.debug("Powermax alarm binding: time {}",
398 String.format("%02d/%02d/%04d %02d:%02d:%02d", cal.get(Calendar.DAY_OF_MONTH),
399 cal.get(Calendar.MONTH) + 1, cal.get(Calendar.YEAR), cal.get(Calendar.HOUR_OF_DAY),
400 cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND)));
402 // Check if time sync was OK
403 if (timeSet != null) {
404 long delta = (timeRead - timeSet) / 1000;
406 logger.debug("Powermax alarm binding: time sync OK (delta {} s)", delta);
408 logger.info("Powermax alarm binding: time sync failed ! (delta {} s)", delta);
412 logger.debug("Cannot get time and date settings");
416 // Process zone names
418 for (int i = 0; i < (26 + customCnt); i++) {
419 String str = readSettingsAsString(PowermaxSendType.DL_ZONESTR, i * 16, (i + 1) * 16 - 1);
422 PowermaxZoneName zoneName = PowermaxZoneName.fromId(i);
423 zoneName.setName(str);
424 } catch (IllegalArgumentException e) {
425 logger.debug("Zone id out of bounds {}", i);
432 logger.debug("Cannot get all zone names");
436 // Process communication settings
438 for (int i = 0; i < phoneNumbers.length; i++) {
439 data = readSettings(PowermaxSendType.DL_PHONENRS, 8 * i, 8 * i + 7);
441 for (int j = 0; j < 8; j++) {
442 if ((data[j] & 0x000000FF) != 0x000000FF) {
444 phoneNumbers[i] = "";
446 if (phoneNumbers[i] != null) {
447 phoneNumbers[i] += String.format("%02X", data[j] & 0x000000FF);
456 logger.debug("Cannot get all communication settings");
460 // Process alarm settings
461 data = readSettings(PowermaxSendType.DL_COMMDEF, 0, 0x1B);
463 bellTime = data[3] & 0x000000FF;
464 silentPanic = (data[0x19] & 0x00000010) == 0x00000010;
465 quickArm = (data[0x1A] & 0x00000008) == 0x00000008;
466 bypassEnabled = (data[0x1B] & 0x000000C0) != 0;
468 logger.debug("Cannot get alarm settings");
472 // Process user PIN codes
474 panelType.isPowerMaster() ? PowermaxSendType.DL_MR_PINCODES : PowermaxSendType.DL_PINCODES, 0,
477 for (int i = 0; i < userCnt; i++) {
478 pinCodes[i] = String.format("%02X%02X", data[i * 2] & 0x000000FF, data[i * 2 + 1] & 0x000000FF);
481 logger.debug("Cannot get PIN codes");
485 // Process EEPROM version
486 panelEprom = readSettingsAsString(PowermaxSendType.DL_PANELFW, 0, 15);
487 if (panelEprom == null) {
488 logger.debug("Cannot get EEPROM version");
492 // Process software version
493 panelSoftware = readSettingsAsString(PowermaxSendType.DL_PANELFW, 16, 31);
494 if (panelSoftware == null) {
495 logger.debug("Cannot get software version");
501 data = readSettings(PowermaxSendType.DL_SERIAL, 0, 5);
503 for (int i = 0; i <= 5; i++) {
504 if ((data[i] & 0x000000FF) != 0x000000FF) {
505 panelSerial += String.format("%02X", data[i] & 0x000000FF);
511 logger.debug("Cannot get serial ID");
515 // Check if partitions are enabled
516 byte[] partitions = readSettings(PowermaxSendType.DL_PARTITIONS, 0, 0x10 + zoneCnt);
517 if (partitions != null) {
518 partitionsEnabled = (partitions[0] & 0x000000FF) == 1;
520 logger.debug("Cannot get partitions information");
523 if (!partitionsEnabled) {
527 // Process zone settings
528 data = readSettings(PowermaxSendType.DL_ZONES, 0, zoneCnt * 4 - 1);
529 byte[] zoneNr = null;
530 byte[] dataMr = null;
531 if (panelType.isPowerMaster()) {
532 zoneNr = readSettings(PowermaxSendType.DL_MR_ZONENAMES, 0, zoneCnt - 1);
533 dataMr = readSettings(PowermaxSendType.DL_MR_ZONES, 0, zoneCnt * 10 - 2);
535 zoneNr = readSettings(PowermaxSendType.DL_ZONENAMES, 0, zoneCnt - 1);
537 if ((data != null) && (zoneNr != null)) {
538 byte[] zero3 = new byte[] { 0, 0, 0 };
539 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
541 for (int i = 0; i < zoneCnt; i++) {
544 PowermaxZoneName zone = PowermaxZoneName.fromId(zoneNr[i] & 0x0000001F);
545 zoneName = zone.getName();
546 } catch (IllegalArgumentException e) {
547 logger.debug("Zone id out of bounds {}", zoneNr[i] & 0x0000001F);
551 boolean zoneEnrolled;
554 String sensorTypeStr;
555 if (panelType.isPowerMaster()) {
556 zoneEnrolled = !Arrays.equals(Arrays.copyOfRange(dataMr, i * 10 + 4, i * 10 + 9), zero5);
558 sensorTypeCode = dataMr[i * 10 + 5];
560 PowermasterSensorType sensorType = PowermasterSensorType.fromCode(sensorTypeCode);
561 sensorTypeStr = sensorType.getLabel();
562 } catch (IllegalArgumentException e) {
563 sensorTypeStr = null;
566 zoneEnrolled = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
567 zoneInfo = data[i * 4 + 3];
568 sensorTypeCode = data[i * 4 + 2];
570 PowermaxSensorType sensorType = PowermaxSensorType
571 .fromCode((byte) (sensorTypeCode & 0x0000000F));
572 sensorTypeStr = sensorType.getLabel();
573 } catch (IllegalArgumentException e) {
574 sensorTypeStr = null;
578 byte zoneType = (byte) (zoneInfo & 0x0000000F);
579 byte zoneChime = (byte) ((zoneInfo >> 4) & 0x00000003);
581 boolean[] part = new boolean[partitionCnt];
582 if (partitionCnt > 1) {
583 for (int j = 0; j < partitionCnt; j++) {
584 part[j] = (partitions != null) ? ((partitions[0x11 + i] & (1 << j)) != 0) : true;
590 zoneSettings[i] = new PowermaxZoneSettings(zoneName, zoneType, zoneChime, sensorTypeStr, part);
594 logger.debug("Cannot get zone settings");
598 data = readSettings(PowermaxSendType.DL_PGMX10, 0, 148);
599 zoneNr = readSettings(PowermaxSendType.DL_X10NAMES, 0, NB_PGM_X10_DEVICES - 2);
600 if ((data != null) && (zoneNr != null)) {
601 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
602 boolean enabled = false;
603 String zoneName = null;
604 for (int j = 0; j <= 8; j++) {
605 if (data[5 + i + j * 0x10] != 0) {
612 PowermaxZoneName zone = PowermaxZoneName.fromId(zoneNr[i - 1] & 0x0000001F);
613 zoneName = zone.getName();
614 } catch (IllegalArgumentException e) {
615 logger.debug("Zone id out of bounds {}", zoneNr[i - 1] & 0x0000001F);
619 x10Settings[i] = new PowermaxX10Settings(zoneName, enabled);
622 logger.debug("Cannot get PGM / X10 settings");
626 if (panelType.isPowerMaster()) {
627 // Process 2 way keypad settings
628 data = readSettings(PowermaxSendType.DL_MR_KEYPADS, 0, keypad2wCnt * 10 - 1);
630 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
632 for (int i = 0; i < keypad2wCnt; i++) {
633 keypad2wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 10 + 4, i * 10 + 9), zero5);
636 logger.debug("Cannot get 2 way keypad settings");
639 // Process siren settings
640 data = readSettings(PowermaxSendType.DL_MR_SIRENS, 0, sirenCnt * 10 - 1);
642 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
644 for (int i = 0; i < sirenCnt; i++) {
645 sirensEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 10 + 4, i * 10 + 9), zero5);
648 logger.debug("Cannot get siren settings");
652 // Process 1 way keypad settings
653 data = readSettings(PowermaxSendType.DL_1WKEYPAD, 0, keypad1wCnt * 4 - 1);
655 byte[] zero2 = new byte[] { 0, 0 };
657 for (int i = 0; i < keypad1wCnt; i++) {
658 keypad1wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 2), zero2);
661 logger.debug("Cannot get 1 way keypad settings");
664 // Process 2 way keypad settings
665 data = readSettings(PowermaxSendType.DL_2WKEYPAD, 0, keypad2wCnt * 4 - 1);
667 byte[] zero3 = new byte[] { 0, 0, 0 };
669 for (int i = 0; i < keypad2wCnt; i++) {
670 keypad2wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
673 logger.debug("Cannot get 2 way keypad settings");
676 // Process siren settings
677 data = readSettings(PowermaxSendType.DL_SIRENS, 0, sirenCnt * 4 - 1);
679 byte[] zero3 = new byte[] { 0, 0, 0 };
681 for (int i = 0; i < sirenCnt; i++) {
682 sirensEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
685 logger.debug("Cannot get siren settings");
690 if (!partitionsEnabled) {
693 boolean[] part = new boolean[partitionCnt];
694 for (int j = 0; j < partitionCnt; j++) {
697 for (int i = 0; i < zoneCnt; i++) {
698 zoneSettings[i] = new PowermaxZoneSettings(null, (byte) 0xFF, (byte) 0xFF, null, part);
700 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
701 x10Settings[i] = new PowermaxX10Settings(null, true);
709 * Update the name of a zone
711 * @param zoneIdx the zone index (first zone is index 1)
712 * @param zoneNameIdx the index in the table of zone names
714 public void updateZoneName(int zoneIdx, byte zoneNameIdx) {
715 PowermaxZoneSettings zone = getZoneSettings(zoneIdx);
719 PowermaxZoneName zoneName = PowermaxZoneName.fromId(zoneNameIdx & 0x0000001F);
720 name = zoneName.getName();
721 } catch (IllegalArgumentException e) {
722 logger.debug("Zone id out of bounds {}", zoneNameIdx & 0x0000001F);
730 * Update the type of a zone
732 * @param zoneIdx the zone index (first zone is index 1)
733 * @param zoneInfo the zone info as an internal code
735 public void updateZoneInfo(int zoneIdx, int zoneInfo) {
736 PowermaxZoneSettings zone = getZoneSettings(zoneIdx);
738 zone.setType((byte) (zoneInfo & 0x0000000F));
742 public String getInfo() {
743 String str = "\nPanel is of type " + panelType.getLabel();
745 int zoneCnt = panelType.getWireless() + panelType.getWired();
746 int partitionCnt = panelType.getPartitions();
747 int sirenCnt = panelType.getSirens();
748 int keypad1wCnt = panelType.getKeypads1w();
749 int keypad2wCnt = panelType.getKeypads2w();
750 // int customCnt = panelType.getCustomZones();
752 if (!partitionsEnabled) {
756 // for (int i = 0; i < (26 + customCnt); i++) {
759 // PowermaxZoneName zoneName = PowermaxZoneName.fromId(i);
760 // name = zoneName.getName();
761 // } catch (IllegalArgumentException e) {
762 // logger.debug("Zone id out of bounds {}", i);
765 // str += String.format("\nZone name %d; %s", i + 1, name);
767 for (int i = 0; i < phoneNumbers.length; i++) {
768 if (phoneNumbers[i] != null) {
769 str += String.format("\nPhone number %d: %s", i + 1, phoneNumbers[i]);
772 str += String.format("\nBell time: %d minutes", bellTime);
773 str += String.format("\nSilent panic: %s", silentPanic ? "enabled" : "disabled");
774 str += String.format("\nQuick arm: %s", quickArm ? "enabled" : "disabled");
775 str += String.format("\nZone bypass: %s", bypassEnabled ? "enabled" : "disabled");
776 str += String.format("\nEPROM: %s", (panelEprom != null) ? panelEprom : "Undefined");
777 str += String.format("\nSW: %s", (panelSoftware != null) ? panelSoftware : "Undefined");
778 str += String.format("\nSerial: %s", (panelSerial != null) ? panelSerial : "Undefined");
779 str += String.format("\nUse partitions: %s", partitionsEnabled ? "enabled" : "disabled");
780 str += String.format("\nNumber of partitions: %d", partitionCnt);
781 for (int i = 0; i < zoneCnt; i++) {
782 if (zoneSettings[i] != null) {
784 for (int j = 1; j <= partitionCnt; j++) {
785 if (zoneSettings[i].isInPartition(j)) {
789 str += String.format("\nZone %d %s: %s (chime = %s; sensor type = %s; partitions = %s)", i + 1,
790 zoneSettings[i].getName(), zoneSettings[i].getType(), zoneSettings[i].getChime(),
791 zoneSettings[i].getSensorType(), partStr);
794 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
795 if (x10Settings[i] != null && x10Settings[i].isEnabled()) {
796 str += String.format("\n%s: %s enabled", (i == 0) ? "PGM" : ("X10 " + i),
797 (x10Settings[i].getName() != null) ? x10Settings[i].getName() : "");
800 for (int i = 1; i <= sirenCnt; i++) {
801 if (isSirenEnrolled(i)) {
802 str += String.format("\nSiren %d enrolled", i);
805 for (int i = 1; i <= keypad1wCnt; i++) {
806 if (isKeypad1wEnrolled(i)) {
807 str += String.format("\nKeypad 1w %d enrolled", i);
810 for (int i = 1; i <= keypad2wCnt; i++) {
811 if (isKeypad2wEnrolled(i)) {
812 str += String.format("\nKeypad 2w %d enrolled", i);
815 return "Powermax alarm binding:" + str;