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.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.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.powermax.internal.message.PowermaxMessageConstants;
25 import org.openhab.core.i18n.TimeZoneProvider;
26 import org.openhab.core.library.types.OnOffType;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.types.UnDefType;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * A class to store the state of the alarm system
35 * @author Laurent Garnier - Initial contribution
38 public class PowermaxState extends PowermaxStateContainer {
40 private final Logger logger = LoggerFactory.getLogger(PowermaxState.class);
42 // For values that are mapped to channels, use a channel name constant from
43 // PowermaxBindingConstants. For values used internally but not mapped to
44 // channels, use a unique name starting with "_".
46 public BooleanValue powerlinkMode = new BooleanValue(this, "_powerlink_mode");
47 public BooleanValue downloadMode = new BooleanValue(this, "_download_mode");
48 public BooleanValue ready = new BooleanValue(this, READY);
49 public BooleanValue bypass = new BooleanValue(this, WITH_ZONES_BYPASSED);
50 public BooleanValue alarmActive = new BooleanValue(this, ALARM_ACTIVE);
51 public BooleanValue trouble = new BooleanValue(this, TROUBLE);
52 public BooleanValue alertInMemory = new BooleanValue(this, ALERT_IN_MEMORY);
53 public BooleanValue ringing = new BooleanValue(this, RINGING);
54 public DateTimeValue ringingSince = new DateTimeValue(this, "_ringing_since");
55 public StringValue statusStr = new StringValue(this, SYSTEM_STATUS);
56 public StringValue armMode = new StringValue(this, "_arm_mode");
57 public BooleanValue downloadSetupRequired = new BooleanValue(this, "_download_setup_required");
58 public DateTimeValue lastKeepAlive = new DateTimeValue(this, "_last_keepalive");
59 public DateTimeValue lastMessageTime = new DateTimeValue(this, LAST_MESSAGE_TIME);
61 public DynamicValue<Boolean> isArmed = new DynamicValue<>(this, SYSTEM_ARMED, () -> {
64 Boolean isArmed = isArmed();
65 if (isArmed == null) {
66 return UnDefType.NULL;
68 return isArmed ? OnOffType.ON : OnOffType.OFF;
71 public DynamicValue<String> panelMode = new DynamicValue<>(this, MODE, () -> {
72 return getPanelMode();
74 String mode = getPanelMode();
76 return UnDefType.NULL;
78 return new StringType(mode);
81 public DynamicValue<String> shortArmMode = new DynamicValue<>(this, ARM_MODE, () -> {
82 return getShortArmMode();
84 String mode = getShortArmMode();
86 return UnDefType.NULL;
88 return new StringType(mode);
91 public DynamicValue<String> activeAlerts = new DynamicValue<>(this, ACTIVE_ALERTS, () -> {
92 return getActiveAlerts();
94 return new StringType(getActiveAlerts());
97 public DynamicValue<Boolean> pgmStatus = new DynamicValue<>(this, PGM_STATUS, () -> {
98 return getPGMX10DeviceStatus(0);
100 Boolean status = getPGMX10DeviceStatus(0);
101 if (status == null) {
102 return UnDefType.NULL;
104 return status ? OnOffType.ON : OnOffType.OFF;
107 private PowermaxPanelSettings panelSettings;
108 private PowermaxZoneState[] zones;
109 private Boolean[] pgmX10DevicesStatus;
110 private byte @Nullable [] updateSettings;
111 private String @Nullable [] eventLog;
112 private Map<Integer, Byte> updatedZoneNames;
113 private Map<Integer, Integer> updatedZoneInfos;
114 private List<PowermaxActiveAlert> activeAlertList;
115 private List<PowermaxActiveAlert> activeAlertQueue;
117 private enum PowermaxAlertAction {
123 private class PowermaxActiveAlert {
124 public final @Nullable PowermaxAlertAction action;
125 public final int zone;
126 public final int code;
128 public PowermaxActiveAlert(@Nullable PowermaxAlertAction action, int zone, int code) {
129 this.action = action;
136 * Constructor (default values)
138 public PowermaxState(PowermaxPanelSettings panelSettings, TimeZoneProvider timeZoneProvider) {
139 super(timeZoneProvider);
140 this.panelSettings = panelSettings;
142 zones = new PowermaxZoneState[panelSettings.getNbZones()];
143 for (int i = 0; i < panelSettings.getNbZones(); i++) {
144 zones[i] = new PowermaxZoneState(timeZoneProvider);
146 pgmX10DevicesStatus = new Boolean[panelSettings.getNbPGMX10Devices()];
147 updatedZoneNames = new HashMap<>();
148 updatedZoneInfos = new HashMap<>();
149 activeAlertList = new ArrayList<>();
150 activeAlertQueue = new ArrayList<>();
152 // Most fields will get populated by the initial download, but we set
153 // the ringing indicator in response to an alarm message. We have no
154 // other way to know if the siren is ringing so we'll initialize it to
157 this.ringing.setValue(false);
161 * Return the PowermaxZoneState object for a given zone. If the zone number is
162 * out of range, returns a dummy PowermaxZoneState object that won't be
163 * persisted. The return value is never null, so it's safe to chain method
166 * @param zone the index of the zone (first zone is index 1)
167 * @return the zone state object (or a dummy zone state)
169 public PowermaxZoneState getZone(int zone) {
170 if ((zone < 1) || (zone > zones.length)) {
171 logger.warn("Received update for invalid zone {}", zone);
172 return new PowermaxZoneState(timeZoneProvider);
174 return zones[zone - 1];
179 * Get the status of a PGM or X10 device
181 * @param device the index of the PGM/X10 device (0 s for PGM; for X10 device is index 1)
183 * @return the status (true or false)
185 public @Nullable Boolean getPGMX10DeviceStatus(int device) {
186 return ((device < 0) || (device >= pgmX10DevicesStatus.length)) ? null : pgmX10DevicesStatus[device];
190 * Set the status of a PGM or X10 device
192 * @param device the index of the PGM/X10 device (0 s for PGM; for X10 device is index 1)
193 * @param status true or false
195 public void setPGMX10DeviceStatus(int device, @Nullable Boolean status) {
196 if ((device >= 0) && (device < pgmX10DevicesStatus.length)) {
197 this.pgmX10DevicesStatus[device] = status;
202 * Get the raw buffer containing all the settings
204 * @return the raw buffer as a table of bytes
206 public byte @Nullable [] getUpdateSettings() {
207 return updateSettings;
211 * Set the raw buffer containing all the settings
213 * @param updateSettings the raw buffer as a table of bytes
215 public void setUpdateSettings(byte[] updateSettings) {
216 this.updateSettings = updateSettings;
220 * Get the number of entries in the event log
222 * @return the number of entries
224 public int getEventLogSize() {
225 String @Nullable [] localEventLog = eventLog;
226 return (localEventLog == null) ? 0 : localEventLog.length;
230 * Set the number of entries in the event log
232 * @param size the number of entries
234 public void setEventLogSize(int size) {
235 eventLog = new String[size];
239 * Get one entry from the event logs
241 * @param index the entry index (1 for the most recent entry)
243 * @return the entry value (event)
245 public @Nullable String getEventLog(int index) {
246 String @Nullable [] localEventLog = eventLog;
247 return ((localEventLog == null) || (index < 1) || (index > getEventLogSize())) ? null
248 : localEventLog[index - 1];
252 * Set one entry from the event logs
254 * @param index the entry index (1 for the most recent entry)
255 * @param event the entry value (event)
257 public void setEventLog(int index, String event) {
258 String @Nullable [] localEventLog = eventLog;
259 if ((localEventLog != null) && (index >= 1) && (index <= getEventLogSize())) {
260 localEventLog[index - 1] = event;
264 public Map<Integer, Byte> getUpdatedZoneNames() {
265 return updatedZoneNames;
268 public void updateZoneName(int zoneIdx, byte zoneNameIdx) {
269 this.updatedZoneNames.put(zoneIdx, zoneNameIdx);
272 public Map<Integer, Integer> getUpdatedZoneInfos() {
273 return updatedZoneInfos;
276 public void updateZoneInfo(int zoneIdx, int zoneInfo) {
277 this.updatedZoneInfos.put(zoneIdx, zoneInfo);
280 // This is an attempt to add persistence to an otherwise (mostly) stateless class.
281 // All of the other values are either present or null, and it's easy to build a
282 // delta state based only on which values are non-null. But these system events
283 // are different because each event can be set by one message and cleared by a
284 // later message. So to preserve the semantics of the state class, we'll keep a
285 // queue of incoming changes, and apply them only when the delta state is resolved.
287 public boolean hasActiveAlertsQueued() {
288 return !activeAlertQueue.isEmpty();
291 public String getActiveAlerts() {
292 if (activeAlertList.isEmpty()) {
296 List<String> alerts = new ArrayList<>();
298 activeAlertList.forEach(e -> {
299 String message = PowermaxMessageConstants.getSystemEvent(e.code).toString();
300 String alert = e.zone == 0 ? message
301 : String.format("%s (%s)", message, panelSettings.getZoneOrUserName(e.zone));
306 return String.join(", ", alerts);
309 public void addActiveAlert(int zoneIdx, int code) {
310 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.ADD, zoneIdx, code);
311 activeAlertQueue.add(alert);
314 public void clearActiveAlert(int zoneIdx, int code) {
315 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR, zoneIdx, code);
316 activeAlertQueue.add(alert);
319 public void clearAllActiveAlerts() {
320 PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR_ALL, 0, 0);
321 activeAlertQueue.add(alert);
324 public void resolveActiveAlerts(@Nullable PowermaxState previousState) {
325 copyActiveAlertsFrom(previousState);
327 activeAlertQueue.forEach(alert -> {
328 if (alert.action == PowermaxAlertAction.CLEAR_ALL) {
329 activeAlertList.clear();
331 activeAlertList.removeIf(e -> e.zone == alert.zone && e.code == alert.code);
333 if (alert.action == PowermaxAlertAction.ADD) {
334 activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
340 private void copyActiveAlertsFrom(@Nullable PowermaxState state) {
341 activeAlertList = new ArrayList<>();
344 state.activeAlertList.forEach(alert -> {
345 activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
353 * @return either Download or Powerlink or Standard
355 public @Nullable String getPanelMode() {
357 if (Boolean.TRUE.equals(downloadMode.getValue())) {
359 } else if (Boolean.TRUE.equals(powerlinkMode.getValue())) {
361 } else if (Boolean.FALSE.equals(powerlinkMode.getValue())) {
368 * Get whether or not the current arming mode is considered as armed
370 * @return true or false
372 public @Nullable Boolean isArmed() {
373 return isArmed(armMode.getValue());
377 * Get whether or not an arming mode is considered as armed
379 * @param armMode the arming mode
381 * @return true or false; null if mode is unexpected
383 private static @Nullable Boolean isArmed(@Nullable String armMode) {
384 Boolean result = null;
385 if (armMode != null) {
387 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
388 result = mode.isArmed();
389 } catch (IllegalArgumentException e) {
390 result = Boolean.FALSE;
397 * Get the short description associated to the current arming mode
399 * @return the short description
401 public @Nullable String getShortArmMode() {
402 return getShortArmMode(armMode.getValue());
406 * Get the short name associated to an arming mode
408 * @param armMode the arming mode
410 * @return the short name or null if mode is unexpected
412 private static @Nullable String getShortArmMode(@Nullable String armMode) {
413 String result = null;
414 if (armMode != null) {
416 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
417 result = mode.getShortName();
418 } catch (IllegalArgumentException e) {
426 * Keep only data that are different from another state and reset all others data to undefined
428 * @param otherState the other state
430 public void keepOnlyDifferencesWith(PowermaxState otherState) {
431 for (int zone = 1; zone <= zones.length; zone++) {
432 PowermaxZoneState thisZone = getZone(zone);
433 PowermaxZoneState otherZone = otherState.getZone(zone);
435 for (int i = 0; i < thisZone.getValues().size(); i++) {
436 Value<?> thisValue = thisZone.getValues().get(i);
437 Value<?> otherValue = otherZone.getValues().get(i);
439 if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
440 thisValue.setValue(null);
445 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
446 Boolean status = getPGMX10DeviceStatus(i);
447 if ((status != null) && status.equals(otherState.getPGMX10DeviceStatus(i))) {
448 setPGMX10DeviceStatus(i, null);
452 for (int i = 0; i < getValues().size(); i++) {
453 Value<?> thisValue = getValues().get(i);
454 Value<?> otherValue = otherState.getValues().get(i);
456 if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
457 thisValue.setValue(null);
461 if (hasActiveAlertsQueued()) {
462 resolveActiveAlerts(otherState);
467 * Update (override) the current state data from another state, ignoring in this other state
470 * @param update the other state to consider for the update
472 public void merge(PowermaxState update) {
473 for (int zone = 1; zone <= zones.length; zone++) {
474 PowermaxZoneState thisZone = getZone(zone);
475 PowermaxZoneState otherZone = update.getZone(zone);
477 for (int i = 0; i < thisZone.getValues().size(); i++) {
478 Value<?> thisValue = thisZone.getValues().get(i);
479 Value<?> otherValue = otherZone.getValues().get(i);
481 if (otherValue.getValue() != null) {
482 thisValue.setValueUnsafe(otherValue.getValue());
487 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
488 Boolean status = update.getPGMX10DeviceStatus(i);
489 if (status != null) {
490 setPGMX10DeviceStatus(i, status);
494 for (int i = 0; i < getValues().size(); i++) {
495 Value<?> thisValue = getValues().get(i);
496 Value<?> otherValue = update.getValues().get(i);
498 if (otherValue.getValue() != null) {
499 thisValue.setValueUnsafe(otherValue.getValue());
503 if (update.getEventLogSize() > getEventLogSize()) {
504 setEventLogSize(update.getEventLogSize());
506 for (int i = 1; i <= getEventLogSize(); i++) {
507 String log = update.getEventLog(i);
513 if (update.hasActiveAlertsQueued()) {
514 copyActiveAlertsFrom(update);
519 public String toString() {
520 String str = "Bridge state:";
522 for (Value<?> value : getValues()) {
523 if (value.getValue() != null) {
524 String channel = value.getChannel();
525 String vStr = value.getValue().toString();
526 String state = value.getState().toString();
528 str += "\n - " + channel + " = " + vStr;
529 if (!vStr.equals(state)) {
530 str += " (" + state + ")";
535 for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
536 Boolean status = getPGMX10DeviceStatus(i);
537 if (status != null) {
538 str += String.format("\n - %s status = %s", (i == 0) ? "PGM device" : String.format("X10 device %d", i),
539 status ? "ON" : "OFF");
543 for (int i = 1; i <= zones.length; i++) {
544 for (Value<?> value : zones[i - 1].getValues()) {
545 if (value.getValue() != null) {
546 String channel = value.getChannel();
547 String vStr = value.getValue().toString();
548 String state = value.getState().toString();
550 str += String.format("\n - sensor zone %d %s = %s", i, channel, vStr);
551 if (!vStr.equals(state)) {
552 str += " (" + state + ")";
558 for (int i = 1; i <= getEventLogSize(); i++) {
559 String log = getEventLog(i);
561 str += "\n - event log " + i + " = " + log;
565 str += "\n - active alarms/alerts = " + getActiveAlerts();