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 (only on panels that support partitions)
516 byte[] partitions = null;
517 if (partitionCnt > 1) {
518 partitions = readSettings(PowermaxSendType.DL_PARTITIONS, 0, 0x10 + zoneCnt);
519 if (partitions != null) {
520 partitionsEnabled = (partitions[0] & 0x000000FF) == 1;
522 logger.debug("Cannot get partitions information");
525 if (!partitionsEnabled) {
530 // Process zone settings
531 data = readSettings(PowermaxSendType.DL_ZONES, 0, zoneCnt * 4 - 1);
532 byte[] zoneNr = null;
533 byte[] dataMr = null;
534 if (panelType.isPowerMaster()) {
535 zoneNr = readSettings(PowermaxSendType.DL_MR_ZONENAMES, 0, zoneCnt - 1);
536 dataMr = readSettings(PowermaxSendType.DL_MR_ZONES, 0, zoneCnt * 10 - 2);
538 zoneNr = readSettings(PowermaxSendType.DL_ZONENAMES, 0, zoneCnt - 1);
540 if ((data != null) && (zoneNr != null)) {
541 byte[] zero3 = new byte[] { 0, 0, 0 };
542 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
544 for (int i = 0; i < zoneCnt; i++) {
547 PowermaxZoneName zone = PowermaxZoneName.fromId(zoneNr[i] & 0x0000001F);
548 zoneName = zone.getName();
549 } catch (IllegalArgumentException e) {
550 logger.debug("Zone id out of bounds {}", zoneNr[i] & 0x0000001F);
554 boolean zoneEnrolled;
557 String sensorTypeStr;
558 if (panelType.isPowerMaster()) {
559 zoneEnrolled = !Arrays.equals(Arrays.copyOfRange(dataMr, i * 10 + 4, i * 10 + 9), zero5);
561 sensorTypeCode = dataMr[i * 10 + 5];
563 PowermasterSensorType sensorType = PowermasterSensorType.fromCode(sensorTypeCode);
564 sensorTypeStr = sensorType.getLabel();
565 } catch (IllegalArgumentException e) {
566 sensorTypeStr = null;
569 zoneEnrolled = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
570 zoneInfo = data[i * 4 + 3];
571 sensorTypeCode = data[i * 4 + 2];
573 PowermaxSensorType sensorType = PowermaxSensorType
574 .fromCode((byte) (sensorTypeCode & 0x0000000F));
575 sensorTypeStr = sensorType.getLabel();
576 } catch (IllegalArgumentException e) {
577 sensorTypeStr = null;
581 byte zoneType = (byte) (zoneInfo & 0x0000000F);
582 byte zoneChime = (byte) ((zoneInfo >> 4) & 0x00000003);
584 boolean[] part = new boolean[partitionCnt];
585 if (partitionCnt > 1) {
586 for (int j = 0; j < partitionCnt; j++) {
587 part[j] = (partitions != null) ? ((partitions[0x11 + i] & (1 << j)) != 0) : true;
593 zoneSettings[i] = new PowermaxZoneSettings(zoneName, zoneType, zoneChime, sensorTypeStr, part);
597 logger.debug("Cannot get zone settings");
601 data = readSettings(PowermaxSendType.DL_PGMX10, 0, 148);
602 zoneNr = readSettings(PowermaxSendType.DL_X10NAMES, 0, NB_PGM_X10_DEVICES - 2);
603 if ((data != null) && (zoneNr != null)) {
604 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
605 boolean enabled = false;
606 String zoneName = null;
607 for (int j = 0; j <= 8; j++) {
608 if (data[5 + i + j * 0x10] != 0) {
615 PowermaxZoneName zone = PowermaxZoneName.fromId(zoneNr[i - 1] & 0x0000001F);
616 zoneName = zone.getName();
617 } catch (IllegalArgumentException e) {
618 logger.debug("Zone id out of bounds {}", zoneNr[i - 1] & 0x0000001F);
622 x10Settings[i] = new PowermaxX10Settings(zoneName, enabled);
625 logger.debug("Cannot get PGM / X10 settings");
629 if (panelType.isPowerMaster()) {
630 // Process 2 way keypad settings
631 data = readSettings(PowermaxSendType.DL_MR_KEYPADS, 0, keypad2wCnt * 10 - 1);
633 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
635 for (int i = 0; i < keypad2wCnt; i++) {
636 keypad2wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 10 + 4, i * 10 + 9), zero5);
639 logger.debug("Cannot get 2 way keypad settings");
642 // Process siren settings
643 data = readSettings(PowermaxSendType.DL_MR_SIRENS, 0, sirenCnt * 10 - 1);
645 byte[] zero5 = new byte[] { 0, 0, 0, 0, 0 };
647 for (int i = 0; i < sirenCnt; i++) {
648 sirensEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 10 + 4, i * 10 + 9), zero5);
651 logger.debug("Cannot get siren settings");
655 // Process 1 way keypad settings
656 data = readSettings(PowermaxSendType.DL_1WKEYPAD, 0, keypad1wCnt * 4 - 1);
658 byte[] zero2 = new byte[] { 0, 0 };
660 for (int i = 0; i < keypad1wCnt; i++) {
661 keypad1wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 2), zero2);
664 logger.debug("Cannot get 1 way keypad settings");
667 // Process 2 way keypad settings
668 data = readSettings(PowermaxSendType.DL_2WKEYPAD, 0, keypad2wCnt * 4 - 1);
670 byte[] zero3 = new byte[] { 0, 0, 0 };
672 for (int i = 0; i < keypad2wCnt; i++) {
673 keypad2wEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
676 logger.debug("Cannot get 2 way keypad settings");
679 // Process siren settings
680 data = readSettings(PowermaxSendType.DL_SIRENS, 0, sirenCnt * 4 - 1);
682 byte[] zero3 = new byte[] { 0, 0, 0 };
684 for (int i = 0; i < sirenCnt; i++) {
685 sirensEnrolled[i] = !Arrays.equals(Arrays.copyOfRange(data, i * 4, i * 4 + 3), zero3);
688 logger.debug("Cannot get siren settings");
693 if (!partitionsEnabled) {
696 boolean[] part = new boolean[partitionCnt];
697 for (int j = 0; j < partitionCnt; j++) {
700 for (int i = 0; i < zoneCnt; i++) {
701 zoneSettings[i] = new PowermaxZoneSettings(null, (byte) 0xFF, (byte) 0xFF, null, part);
703 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
704 x10Settings[i] = new PowermaxX10Settings(null, true);
712 * Update the name of a zone
714 * @param zoneIdx the zone index (first zone is index 1)
715 * @param zoneNameIdx the index in the table of zone names
717 public void updateZoneName(int zoneIdx, byte zoneNameIdx) {
718 PowermaxZoneSettings zone = getZoneSettings(zoneIdx);
722 PowermaxZoneName zoneName = PowermaxZoneName.fromId(zoneNameIdx & 0x0000001F);
723 name = zoneName.getName();
724 } catch (IllegalArgumentException e) {
725 logger.debug("Zone id out of bounds {}", zoneNameIdx & 0x0000001F);
733 * Update the type of a zone
735 * @param zoneIdx the zone index (first zone is index 1)
736 * @param zoneInfo the zone info as an internal code
738 public void updateZoneInfo(int zoneIdx, int zoneInfo) {
739 PowermaxZoneSettings zone = getZoneSettings(zoneIdx);
741 zone.setType((byte) (zoneInfo & 0x0000000F));
745 public String getInfo() {
746 String str = "\nPanel is of type " + panelType.getLabel();
748 int zoneCnt = panelType.getWireless() + panelType.getWired();
749 int partitionCnt = panelType.getPartitions();
750 int sirenCnt = panelType.getSirens();
751 int keypad1wCnt = panelType.getKeypads1w();
752 int keypad2wCnt = panelType.getKeypads2w();
753 // int customCnt = panelType.getCustomZones();
755 if (!partitionsEnabled) {
759 // for (int i = 0; i < (26 + customCnt); i++) {
762 // PowermaxZoneName zoneName = PowermaxZoneName.fromId(i);
763 // name = zoneName.getName();
764 // } catch (IllegalArgumentException e) {
765 // logger.debug("Zone id out of bounds {}", i);
768 // str += String.format("\nZone name %d; %s", i + 1, name);
770 for (int i = 0; i < phoneNumbers.length; i++) {
771 if (phoneNumbers[i] != null) {
772 str += String.format("\nPhone number %d: %s", i + 1, phoneNumbers[i]);
775 str += String.format("\nBell time: %d minutes", bellTime);
776 str += String.format("\nSilent panic: %s", silentPanic ? "enabled" : "disabled");
777 str += String.format("\nQuick arm: %s", quickArm ? "enabled" : "disabled");
778 str += String.format("\nZone bypass: %s", bypassEnabled ? "enabled" : "disabled");
779 str += String.format("\nEPROM: %s", (panelEprom != null) ? panelEprom : "Undefined");
780 str += String.format("\nSW: %s", (panelSoftware != null) ? panelSoftware : "Undefined");
781 str += String.format("\nSerial: %s", (panelSerial != null) ? panelSerial : "Undefined");
782 str += String.format("\nUse partitions: %s", partitionsEnabled ? "enabled" : "disabled");
783 str += String.format("\nNumber of partitions: %d", partitionCnt);
784 for (int i = 0; i < zoneCnt; i++) {
785 if (zoneSettings[i] != null) {
787 for (int j = 1; j <= partitionCnt; j++) {
788 if (zoneSettings[i].isInPartition(j)) {
792 str += String.format("\nZone %d %s: %s (chime = %s; sensor type = %s; partitions = %s)", i + 1,
793 zoneSettings[i].getName(), zoneSettings[i].getType(), zoneSettings[i].getChime(),
794 zoneSettings[i].getSensorType(), partStr);
797 for (int i = 0; i < NB_PGM_X10_DEVICES; i++) {
798 if (x10Settings[i] != null && x10Settings[i].isEnabled()) {
799 str += String.format("\n%s: %s enabled", (i == 0) ? "PGM" : ("X10 " + i),
800 (x10Settings[i].getName() != null) ? x10Settings[i].getName() : "");
803 for (int i = 1; i <= sirenCnt; i++) {
804 if (isSirenEnrolled(i)) {
805 str += String.format("\nSiren %d enrolled", i);
808 for (int i = 1; i <= keypad1wCnt; i++) {
809 if (isKeypad1wEnrolled(i)) {
810 str += String.format("\nKeypad 1w %d enrolled", i);
813 for (int i = 1; i <= keypad2wCnt; i++) {
814 if (isKeypad2wEnrolled(i)) {
815 str += String.format("\nKeypad 2w %d enrolled", i);
818 return "Powermax alarm binding:" + str;