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.math.BigInteger;
16 import java.util.ArrayList;
17 import java.util.List;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.powermax.internal.state.PowermaxArmMode;
22 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings;
23 import org.openhab.binding.powermax.internal.state.PowermaxSensorType;
24 import org.openhab.binding.powermax.internal.state.PowermaxState;
25 import org.openhab.binding.powermax.internal.state.PowermaxZoneSettings;
28 * A class for STATUS message handling
30 * @author Laurent Garnier - Initial contribution
33 public class PowermaxStatusMessage extends PowermaxBaseMessage {
35 private static byte[] zoneBytes(byte zones1, byte zones9, byte zones17, byte zones25) {
36 return new byte[] { zones25, zones17, zones9, zones1 };
39 private static boolean[] zoneBits(byte[] zoneBytes) {
40 boolean[] zones = new boolean[33];
41 BigInteger bigint = new BigInteger(1, zoneBytes);
43 for (int i = 1; i <= 32; i++) {
44 zones[i] = bigint.testBit(i - 1);
50 private static String zoneList(byte[] zoneBytes) {
51 boolean[] zones = zoneBits(zoneBytes);
52 List<String> names = new ArrayList<>();
54 for (int i = 1; i < zones.length; i++) {
56 names.add(String.format("Zone %d", i));
60 return String.join(", ", names);
67 * the received message as a buffer of bytes
69 public PowermaxStatusMessage(byte[] message) {
74 protected @Nullable PowermaxState handleMessageInternal(@Nullable PowermaxCommManager commManager) {
75 if (commManager == null) {
79 PowermaxPanelSettings panelSettings = commManager.getPanelSettings();
80 PowermaxState updatedState = commManager.createNewState();
82 byte[] message = getRawData();
83 byte eventType = message[3];
84 String eventTypeStr = PowermaxMessageConstants.getZoneEventType(eventType & 0x000000FF);
86 debug("Event type", eventType, eventTypeStr);
88 // Each event type except 0x04 contains two sets of zone bitmasks.
89 // Each set is four bytes (32 bits) where each bit indicates the state
90 // of the corresponding zone (1 = set, 0 = unset).
92 if (eventType == 0x01) {
93 // These bits are set when a zone causes an alarm
95 // Set 1: Alarm caused by zone being open/tripped
96 // Set 2: Alarm caused by a tamper
98 // Note: active alarms are cleared when the Memory flag is turned off
99 // (the panel won't send a follow-up event with these bits set to zero)
101 byte[] alarmStatusBytes = zoneBytes(message[4], message[5], message[6], message[7]);
102 byte[] tamperStatusBytes = zoneBytes(message[8], message[9], message[10], message[11]);
104 boolean[] alarmStatus = zoneBits(alarmStatusBytes);
105 boolean[] tamperStatus = zoneBits(tamperStatusBytes);
107 String alarmStatusStr = zoneList(alarmStatusBytes);
108 String tamperStatusStr = zoneList(tamperStatusBytes);
110 panelSettings.getZoneRange().forEach(i -> {
111 updatedState.getZone(i).alarmed.setValue(alarmStatus[i]);
112 updatedState.getZone(i).tamperAlarm.setValue(tamperStatus[i]);
115 debug("Alarm status", alarmStatusBytes, alarmStatusStr);
116 debug("Tamper alarm status", tamperStatusBytes, tamperStatusStr);
117 } else if (eventType == 0x02) {
118 // Set 1: List of zones that are open/tripped
119 // Set 2: List of zones that have a low-battery condition
121 byte[] zoneStatusBytes = zoneBytes(message[4], message[5], message[6], message[7]);
122 byte[] batteryStatusBytes = zoneBytes(message[8], message[9], message[10], message[11]);
124 boolean[] zoneStatus = zoneBits(zoneStatusBytes);
125 boolean[] batteryStatus = zoneBits(batteryStatusBytes);
127 String zoneStatusStr = zoneList(zoneStatusBytes);
128 String batteryStatusStr = zoneList(batteryStatusBytes);
130 panelSettings.getZoneRange().forEach(i -> {
131 updatedState.getZone(i).tripped.setValue(zoneStatus[i]);
132 updatedState.getZone(i).lowBattery.setValue(batteryStatus[i]);
135 debug("Zone status", zoneStatusBytes, zoneStatusStr);
136 debug("Battery status", batteryStatusBytes, batteryStatusStr);
137 } else if (eventType == 0x03) {
138 // Set 1: Inactivity / loss of supervision
139 // Set 2: Zone has an active tamper condition
141 byte[] inactiveStatusBytes = zoneBytes(message[4], message[5], message[6], message[7]);
142 byte[] tamperStatusBytes = zoneBytes(message[8], message[9], message[10], message[11]);
144 boolean[] inactiveStatus = zoneBits(inactiveStatusBytes);
145 boolean[] tamperStatus = zoneBits(tamperStatusBytes);
147 String inactiveStatusStr = zoneList(inactiveStatusBytes);
148 String tamperStatusStr = zoneList(tamperStatusBytes);
150 panelSettings.getZoneRange().forEach(i -> {
151 updatedState.getZone(i).inactive.setValue(inactiveStatus[i]);
152 updatedState.getZone(i).tampered.setValue(tamperStatus[i]);
155 debug("Inactive status", inactiveStatusBytes, inactiveStatusStr);
156 debug("Tamper status", tamperStatusBytes, tamperStatusStr);
157 } else if (eventType == 0x04) {
158 // System & zone status message (not like the other event types)
160 byte sysStatus = message[4];
161 byte sysFlags = message[5];
162 int eventZone = message[6] & 0x000000FF;
163 int zoneEType = message[7] & 0x000000FF;
164 int x10Status = (message[10] & 0x000000FF) | ((message[11] << 8) & 0x0000FF00);
166 String eventZoneStr = panelSettings.getZoneOrUserName(eventZone);
167 String zoneETypeStr = PowermaxMessageConstants.getZoneEvent(zoneEType);
169 if (zoneEType != 0x00 && eventZone > 0 && eventZone <= panelSettings.getNbZones()) {
170 updatedState.getZone(eventZone).lastMessage.setValue(zoneETypeStr);
171 updatedState.getZone(eventZone).lastMessageTime.setValue(System.currentTimeMillis());
174 if (zoneEType == 0x03) {
176 updatedState.getZone(eventZone).tripped.setValue(true);
177 updatedState.getZone(eventZone).lastTripped.setValue(System.currentTimeMillis());
178 } else if (zoneEType == 0x04) {
180 updatedState.getZone(eventZone).tripped.setValue(false);
181 } else if (zoneEType == 0x05) {
183 PowermaxZoneSettings zone = panelSettings.getZoneSettings(eventZone);
184 if ((zone != null) && zone.getSensorType().equalsIgnoreCase("unknown")) {
185 zone.setSensorType(PowermaxSensorType.MOTION_SENSOR_1.getLabel());
187 updatedState.getZone(eventZone).tripped.setValue(true);
188 updatedState.getZone(eventZone).lastTripped.setValue(System.currentTimeMillis());
192 for (int i = 0; i < panelSettings.getNbPGMX10Devices(); i++) {
193 updatedState.setPGMX10DeviceStatus(i, ((x10Status >> i) & 0x1) > 0);
196 String sysStatusStr = "";
197 if ((sysFlags & 0x1) == 1) {
198 sysStatusStr = sysStatusStr + "Ready, ";
199 updatedState.ready.setValue(true);
201 sysStatusStr = sysStatusStr + "Not ready, ";
202 updatedState.ready.setValue(false);
204 if (((sysFlags >> 1) & 0x1) == 1) {
205 sysStatusStr = sysStatusStr + "Alert in memory, ";
206 updatedState.alertInMemory.setValue(true);
208 updatedState.alertInMemory.setValue(false);
210 // When the memory flag is cleared, also clear all zone alarms and tamper alarms
211 panelSettings.getZoneRange().forEach(i -> {
212 updatedState.getZone(i).alarmed.setValue(false);
213 updatedState.getZone(i).tamperAlarm.setValue(false);
216 if (((sysFlags >> 2) & 0x1) == 1) {
217 sysStatusStr = sysStatusStr + "Trouble, ";
218 updatedState.trouble.setValue(true);
220 updatedState.trouble.setValue(false);
222 if (((sysFlags >> 3) & 0x1) == 1) {
223 sysStatusStr = sysStatusStr + "Bypass on, ";
224 updatedState.bypass.setValue(true);
226 updatedState.bypass.setValue(false);
227 panelSettings.getZoneRange().forEach(i -> {
228 updatedState.getZone(i).bypassed.setValue(false);
231 if (((sysFlags >> 4) & 0x1) == 1) {
232 sysStatusStr = sysStatusStr + "Last 10 seconds, ";
234 if (((sysFlags >> 5) & 0x1) == 1) {
235 sysStatusStr = sysStatusStr + zoneETypeStr;
236 if (eventZone == 0xFF) {
237 sysStatusStr = sysStatusStr + " from Panel, ";
238 } else if (eventZone > 0) {
239 sysStatusStr = sysStatusStr + String.format(" in %s, ", eventZoneStr);
241 sysStatusStr = sysStatusStr + ", ";
244 if (((sysFlags >> 6) & 0x1) == 1) {
245 sysStatusStr = sysStatusStr + "Status changed, ";
247 if (((sysFlags >> 7) & 0x1) == 1) {
248 sysStatusStr = sysStatusStr + "Alarm event, ";
249 updatedState.alarmActive.setValue(true);
251 updatedState.alarmActive.setValue(false);
253 sysStatusStr = sysStatusStr.substring(0, sysStatusStr.length() - 2);
256 PowermaxArmMode armMode = PowermaxArmMode.fromCode(sysStatus & 0x000000FF);
257 statusStr = armMode.getName();
258 } catch (IllegalArgumentException e) {
259 statusStr = "UNKNOWN";
261 updatedState.armMode.setValue(statusStr);
262 updatedState.statusStr.setValue(statusStr + ", " + sysStatusStr);
264 debug("System status", sysStatus, statusStr);
265 debug("System flags", sysFlags, sysStatusStr);
266 debug("Event zone", eventZone, eventZoneStr);
267 debug("Zone event type", zoneEType, zoneETypeStr);
268 debug("X10 status", x10Status);
270 panelSettings.getZoneRange().forEach(i -> {
271 PowermaxZoneSettings zone = panelSettings.getZoneSettings(i);
273 // mode: armed or not
274 int mode = sysStatus & 0x0000000F;
275 // Zone is shown as armed if
276 // the sensor type always triggers an alarm
277 // or the system is armed away
278 // or the system is armed home and the zone is not interior(-follow)
279 boolean armed = (!zone.getType().equalsIgnoreCase("Non-Alarm")
280 && (zone.isAlwaysInAlarm() || (mode == PowermaxArmMode.ARMED_AWAY.getCode())
281 || ((mode == PowermaxArmMode.ARMED_HOME.getCode())
282 && !zone.getType().equalsIgnoreCase("Interior-Follow")
283 && !zone.getType().equalsIgnoreCase("Interior"))));
284 updatedState.getZone(i).armed.setValue(armed);
287 } else if (eventType == 0x06) {
288 // Set 1: List of zones that are enrolled (we don't currently use this)
289 // Set 2: List of zones that are bypassed
291 byte[] zoneBypassBytes = zoneBytes(message[8], message[9], message[10], message[11]);
292 boolean[] zoneBypass = zoneBits(zoneBypassBytes);
293 String zoneBypassStr = zoneList(zoneBypassBytes);
295 panelSettings.getZoneRange().forEach(i -> {
296 updatedState.getZone(i).bypassed.setValue(zoneBypass[i]);
299 debug("Zone bypass", zoneBypassBytes, zoneBypassStr);
302 // Note: in response to a STATUS request, the panel will also send
303 // messages with eventType = 0x05, 0x07, 0x08, and 0x09 but these
304 // haven't been decoded yet