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 static org.openhab.binding.powermax.internal.PowermaxBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.powermax.internal.message.PowermaxMessageConstants;
24 import org.openhab.core.i18n.TimeZoneProvider;
25 import org.openhab.core.library.types.OnOffType;
26 import org.openhab.core.library.types.StringType;
27 import org.slf4j.Logger;
28 import org.slf4j.LoggerFactory;
31 * A class to store the state of the alarm system
33 * @author Laurent Garnier - Initial contribution
35 public class PowermaxState extends PowermaxStateContainer {
37 private final Logger logger = LoggerFactory.getLogger(PowermaxState.class);
39 // For values that are mapped to channels, use a channel name constant from
40 // PowermaxBindingConstants. For values used internally but not mapped to
41 // channels, use a unique name starting with "_".
43 public BooleanValue powerlinkMode = new BooleanValue(this, "_powerlink_mode");
44 public BooleanValue downloadMode = new BooleanValue(this, "_download_mode");
45 public BooleanValue ready = new BooleanValue(this, READY);
46 public BooleanValue bypass = new BooleanValue(this, WITH_ZONES_BYPASSED);
47 public BooleanValue alarmActive = new BooleanValue(this, ALARM_ACTIVE);
48 public BooleanValue trouble = new BooleanValue(this, TROUBLE);
49 public BooleanValue alertInMemory = new BooleanValue(this, ALERT_IN_MEMORY);
50 public BooleanValue ringing = new BooleanValue(this, RINGING);
51 public DateTimeValue ringingSince = new DateTimeValue(this, "_ringing_since");
52 public StringValue statusStr = new StringValue(this, SYSTEM_STATUS);
53 public StringValue armMode = new StringValue(this, "_arm_mode");
54 public BooleanValue downloadSetupRequired = new BooleanValue(this, "_download_setup_required");
55 public DateTimeValue lastKeepAlive = new DateTimeValue(this, "_last_keepalive");
56 public DateTimeValue lastMessageTime = new DateTimeValue(this, LAST_MESSAGE_TIME);
58 public DynamicValue<Boolean> isArmed = new DynamicValue<>(this, SYSTEM_ARMED, () -> {
61 return isArmed() ? OnOffType.ON : OnOffType.OFF;
64 public DynamicValue<String> panelMode = new DynamicValue<>(this, MODE, () -> {
65 return getPanelMode();
67 return new StringType(getPanelMode());
70 public DynamicValue<String> shortArmMode = new DynamicValue<>(this, ARM_MODE, () -> {
71 return getShortArmMode();
73 return new StringType(getShortArmMode());
76 public DynamicValue<String> activeAlerts = new DynamicValue<>(this, ACTIVE_ALERTS, () -> {
77 return getActiveAlerts();
79 return new StringType(getActiveAlerts());
82 public DynamicValue<Boolean> pgmStatus = new DynamicValue<>(this, PGM_STATUS, () -> {
83 return getPGMX10DeviceStatus(0);
85 return getPGMX10DeviceStatus(0) ? OnOffType.ON : OnOffType.OFF;
88 private PowermaxPanelSettings panelSettings;
89 private PowermaxZoneState[] zones;
90 private Boolean[] pgmX10DevicesStatus;
91 private byte[] updateSettings;
92 private String[] eventLog;
93 private Map<Integer, Byte> updatedZoneNames;
94 private Map<Integer, Integer> updatedZoneInfos;
95 private List<PowermaxActiveAlert> activeAlertList;
96 private List<PowermaxActiveAlert> activeAlertQueue;
98 private enum PowermaxAlertAction {
104 private class PowermaxActiveAlert {
105 public final @Nullable PowermaxAlertAction action;
106 public final int zone;
107 public final int code;
109 public PowermaxActiveAlert(@Nullable PowermaxAlertAction action, int zone, int code) {
110 this.action = action;
117 * Constructor (default values)
119 public PowermaxState(PowermaxPanelSettings panelSettings, TimeZoneProvider timeZoneProvider) {
120 super(timeZoneProvider);
121 this.panelSettings = panelSettings;
123 zones = new PowermaxZoneState[panelSettings.getNbZones()];
124 for (int i = 0; i < panelSettings.getNbZones(); i++) {
125 zones[i] = new PowermaxZoneState(timeZoneProvider);
127 pgmX10DevicesStatus = new Boolean[panelSettings.getNbPGMX10Devices()];
128 updatedZoneNames = new HashMap<>();
129 updatedZoneInfos = new HashMap<>();
130 activeAlertList = new ArrayList<>();
131 activeAlertQueue = new ArrayList<>();
133 // Most fields will get populated by the initial download, but we set
134 // the ringing indicator in response to an alarm message. We have no
135 // other way to know if the siren is ringing so we'll initialize it to
138 this.ringing.setValue(false);
142 * Return the PowermaxZoneState object for a given zone. If the zone number is
143 * out of range, returns a dummy PowermaxZoneState object that won't be
144 * persisted. The return value is never null, so it's safe to chain method
147 * @param zone the index of the zone (first zone is index 1)
148 * @return the zone state object (or a dummy zone state)
150 public PowermaxZoneState getZone(int zone) {
151 if ((zone < 1) || (zone > zones.length)) {
152 logger.warn("Received update for invalid zone {}", zone);
153 return new PowermaxZoneState(timeZoneProvider);
155 return zones[zone - 1];
160 * Get the status of a PGM or X10 device
162 * @param device the index of the PGM/X10 device (0 s for PGM; for X10 device is index 1)
164 * @return the status (true or false)
166 public Boolean getPGMX10DeviceStatus(int device) {
167 return ((device < 0) || (device >= pgmX10DevicesStatus.length)) ? null : pgmX10DevicesStatus[device];
171 * Set the status of a PGM or X10 device
173 * @param device the index of the PGM/X10 device (0 s for PGM; for X10 device is index 1)
174 * @param status true or false
176 public void setPGMX10DeviceStatus(int device, Boolean status) {
177 if ((device >= 0) && (device < pgmX10DevicesStatus.length)) {
178 this.pgmX10DevicesStatus[device] = status;
183 * Get the raw buffer containing all the settings
185 * @return the raw buffer as a table of bytes
187 public byte[] getUpdateSettings() {
188 return updateSettings;
192 * Set the raw buffer containing all the settings
194 * @param updateSettings the raw buffer as a table of bytes
196 public void setUpdateSettings(byte[] updateSettings) {
197 this.updateSettings = updateSettings;
201 * Get the number of entries in the event log
203 * @return the number of entries
205 public int getEventLogSize() {
206 return (eventLog == null) ? 0 : eventLog.length;
210 * Set the number of entries in the event log
212 * @param size the number of entries
214 public void setEventLogSize(int size) {
215 eventLog = new String[size];
219 * Get one entry from the event logs
221 * @param index the entry index (1 for the most recent entry)
223 * @return the entry value (event)
225 public String getEventLog(int index) {
226 return ((index < 1) || (index > getEventLogSize())) ? null : eventLog[index - 1];
230 * Set one entry from the event logs
232 * @param index the entry index (1 for the most recent entry)
233 * @param event the entry value (event)
235 public void setEventLog(int index, String event) {
236 if ((index >= 1) && (index <= getEventLogSize())) {
237 this.eventLog[index - 1] = event;
241 public Map<Integer, Byte> getUpdatedZoneNames() {
242 return updatedZoneNames;
245 public void updateZoneName(int zoneIdx, byte zoneNameIdx) {
246 this.updatedZoneNames.put(zoneIdx, zoneNameIdx);
249 public Map<Integer, Integer> getUpdatedZoneInfos() {
250 return updatedZoneInfos;
253 public void updateZoneInfo(int zoneIdx, int zoneInfo) {
254 this.updatedZoneInfos.put(zoneIdx, zoneInfo);
257 // This is an attempt to add persistence to an otherwise (mostly) stateless class.
258 // All of the other values are either present or null, and it's easy to build a
259 // delta state based only on which values are non-null. But these system events
260 // are different because each event can be set by one message and cleared by a
261 // later message. So to preserve the semantics of the state class, we'll keep a
262 // queue of incoming changes, and apply them only when the delta state is resolved.
264 public boolean hasActiveAlertsQueued() {
265 return !activeAlertQueue.isEmpty();
268 public String getActiveAlerts() {
269 if (activeAlertList.isEmpty()) {
273 List<String> alerts = new ArrayList<>();
275 activeAlertList.forEach(e -> {
276 String message = PowermaxMessageConstants.getSystemEvent(e.code).toString();
277 String alert = e.zone == 0 ? message
278 : String.format("%s (%s)", message, panelSettings.getZoneOrUserName(e.zone));
283 return String.join(", ", alerts);
286 public void addActiveAlert(int zoneIdx, int code) {
287 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.ADD, zoneIdx, code);
288 activeAlertQueue.add(alert);
291 public void clearActiveAlert(int zoneIdx, int code) {
292 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR, zoneIdx, code);
293 activeAlertQueue.add(alert);
296 public void clearAllActiveAlerts() {
297 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR_ALL, 0, 0);
298 activeAlertQueue.add(alert);
301 public void resolveActiveAlerts(@Nullable PowermaxState previousState) {
302 copyActiveAlertsFrom(previousState);
304 activeAlertQueue.forEach(alert -> {
305 if (alert.action == PowermaxAlertAction.CLEAR_ALL) {
306 activeAlertList.clear();
308 activeAlertList.removeIf(e -> e.zone == alert.zone && e.code == alert.code);
310 if (alert.action == PowermaxAlertAction.ADD) {
311 activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
317 private void copyActiveAlertsFrom(@Nullable PowermaxState state) {
318 activeAlertList = new ArrayList<>();
321 state.activeAlertList.forEach(alert -> {
322 activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
330 * @return either Download or Powerlink or Standard
332 public String getPanelMode() {
334 if (Boolean.TRUE.equals(downloadMode.getValue())) {
336 } else if (Boolean.TRUE.equals(powerlinkMode.getValue())) {
338 } else if (Boolean.FALSE.equals(powerlinkMode.getValue())) {
345 * Get whether or not the current arming mode is considered as armed
347 * @return true or false
349 public Boolean isArmed() {
350 return isArmed(armMode.getValue());
354 * Get whether or not an arming mode is considered as armed
356 * @param armMode the arming mode
358 * @return true or false; null if mode is unexpected
360 private static Boolean isArmed(String armMode) {
361 Boolean result = null;
362 if (armMode != null) {
364 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
365 result = mode.isArmed();
366 } catch (IllegalArgumentException e) {
367 result = Boolean.FALSE;
374 * Get the short description associated to the current arming mode
376 * @return the short description
378 public String getShortArmMode() {
379 return getShortArmMode(armMode.getValue());
383 * Get the short name associated to an arming mode
385 * @param armMode the arming mode
387 * @return the short name or null if mode is unexpected
389 private static String getShortArmMode(String armMode) {
390 String result = null;
391 if (armMode != null) {
393 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
394 result = mode.getShortName();
395 } catch (IllegalArgumentException e) {
403 * Keep only data that are different from another state and reset all others data to undefined
405 * @param otherState the other state
407 public void keepOnlyDifferencesWith(PowermaxState otherState) {
408 for (int zone = 1; zone <= zones.length; zone++) {
409 PowermaxZoneState thisZone = getZone(zone);
410 PowermaxZoneState otherZone = otherState.getZone(zone);
412 for (int i = 0; i < thisZone.getValues().size(); i++) {
413 Value<?> thisValue = thisZone.getValues().get(i);
414 Value<?> otherValue = otherZone.getValues().get(i);
416 if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
417 thisValue.setValue(null);
422 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
423 if ((getPGMX10DeviceStatus(i) != null)
424 && getPGMX10DeviceStatus(i).equals(otherState.getPGMX10DeviceStatus(i))) {
425 setPGMX10DeviceStatus(i, null);
429 for (int i = 0; i < getValues().size(); i++) {
430 Value<?> thisValue = getValues().get(i);
431 Value<?> otherValue = otherState.getValues().get(i);
433 if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
434 thisValue.setValue(null);
438 if (hasActiveAlertsQueued()) {
439 resolveActiveAlerts(otherState);
444 * Update (override) the current state data from another state, ignoring in this other state
447 * @param update the other state to consider for the update
449 public void merge(PowermaxState update) {
450 for (int zone = 1; zone <= zones.length; zone++) {
451 PowermaxZoneState thisZone = getZone(zone);
452 PowermaxZoneState otherZone = update.getZone(zone);
454 for (int i = 0; i < thisZone.getValues().size(); i++) {
455 Value<?> thisValue = thisZone.getValues().get(i);
456 Value<?> otherValue = otherZone.getValues().get(i);
458 if (otherValue.getValue() != null) {
459 thisValue.setValueUnsafe(otherValue.getValue());
464 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
465 if (update.getPGMX10DeviceStatus(i) != null) {
466 setPGMX10DeviceStatus(i, update.getPGMX10DeviceStatus(i));
470 for (int i = 0; i < getValues().size(); i++) {
471 Value<?> thisValue = getValues().get(i);
472 Value<?> otherValue = update.getValues().get(i);
474 if (otherValue.getValue() != null) {
475 thisValue.setValueUnsafe(otherValue.getValue());
479 if (update.getEventLogSize() > getEventLogSize()) {
480 setEventLogSize(update.getEventLogSize());
482 for (int i = 1; i <= getEventLogSize(); i++) {
483 if (update.getEventLog(i) != null) {
484 setEventLog(i, update.getEventLog(i));
488 if (update.hasActiveAlertsQueued()) {
489 copyActiveAlertsFrom(update);
494 public String toString() {
495 String str = "Bridge state:";
497 for (Value<?> value : getValues()) {
498 if ((value.getChannel() != null) && (value.getValue() != null)) {
499 String channel = value.getChannel();
500 String vStr = value.getValue().toString();
501 String state = value.getState().toString();
503 str += "\n - " + channel + " = " + vStr;
504 if (!vStr.equals(state)) {
505 str += " (" + state + ")";
510 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
511 if (getPGMX10DeviceStatus(i) != null) {
512 str += String.format("\n - %s status = %s", (i == 0) ? "PGM device" : String.format("X10 device %d", i),
513 getPGMX10DeviceStatus(i) ? "ON" : "OFF");
517 for (int i = 1; i <= zones.length; i++) {
518 for (Value<?> value : zones[i - 1].getValues()) {
519 if ((value.getChannel() != null) && (value.getValue() != null)) {
520 String channel = value.getChannel();
521 String vStr = value.getValue().toString();
522 String state = value.getState().toString();
524 str += String.format("\n - sensor zone %d %s = %s", i, channel, vStr);
525 if (!vStr.equals(state)) {
526 str += " (" + state + ")";
532 for (int i = 1; i <= getEventLogSize(); i++) {
533 if (getEventLog(i) != null) {
534 str += "\n - event log " + i + " = " + getEventLog(i);
538 String alarms = getActiveAlerts();
539 str += "\n - active alarms/alerts = " + (alarms == null ? "null" : alarms);