]> git.basschouten.com Git - openhab-addons.git/blob
73448ec65f49aad1f38d7aab9f0e968abc22a2de
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.powermax.internal.state;
14
15 import static org.openhab.binding.powermax.internal.PowermaxBindingConstants.*;
16
17 import java.util.ArrayList;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21
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;
31
32 /**
33  * A class to store the state of the alarm system
34  *
35  * @author Laurent Garnier - Initial contribution
36  */
37 @NonNullByDefault
38 public class PowermaxState extends PowermaxStateContainer {
39
40     private final Logger logger = LoggerFactory.getLogger(PowermaxState.class);
41
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 "_".
45
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);
60
61     public DynamicValue<Boolean> isArmed = new DynamicValue<>(this, SYSTEM_ARMED, () -> {
62         return isArmed();
63     }, () -> {
64         Boolean isArmed = isArmed();
65         if (isArmed == null) {
66             return UnDefType.NULL;
67         }
68         return OnOffType.from(isArmed);
69     });
70
71     public DynamicValue<String> panelMode = new DynamicValue<>(this, MODE, () -> {
72         return getPanelMode();
73     }, () -> {
74         String mode = getPanelMode();
75         if (mode == null) {
76             return UnDefType.NULL;
77         }
78         return new StringType(mode);
79     });
80
81     public DynamicValue<String> shortArmMode = new DynamicValue<>(this, ARM_MODE, () -> {
82         return getShortArmMode();
83     }, () -> {
84         String mode = getShortArmMode();
85         if (mode == null) {
86             return UnDefType.NULL;
87         }
88         return new StringType(mode);
89     });
90
91     public DynamicValue<String> activeAlerts = new DynamicValue<>(this, ACTIVE_ALERTS, () -> {
92         return getActiveAlerts();
93     }, () -> {
94         return new StringType(getActiveAlerts());
95     });
96
97     public DynamicValue<Boolean> pgmStatus = new DynamicValue<>(this, PGM_STATUS, () -> {
98         return getPGMX10DeviceStatus(0);
99     }, () -> {
100         Boolean status = getPGMX10DeviceStatus(0);
101         if (status == null) {
102             return UnDefType.NULL;
103         }
104         return OnOffType.from(status);
105     });
106
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;
116
117     private enum PowermaxAlertAction {
118         ADD,
119         CLEAR,
120         CLEAR_ALL
121     }
122
123     private class PowermaxActiveAlert {
124         public final @Nullable PowermaxAlertAction action;
125         public final int zone;
126         public final int code;
127
128         public PowermaxActiveAlert(@Nullable PowermaxAlertAction action, int zone, int code) {
129             this.action = action;
130             this.zone = zone;
131             this.code = code;
132         }
133     }
134
135     /**
136      * Constructor (default values)
137      */
138     public PowermaxState(PowermaxPanelSettings panelSettings, TimeZoneProvider timeZoneProvider) {
139         super(timeZoneProvider);
140         this.panelSettings = panelSettings;
141
142         zones = new PowermaxZoneState[panelSettings.getNbZones()];
143         for (int i = 0; i < panelSettings.getNbZones(); i++) {
144             zones[i] = new PowermaxZoneState(timeZoneProvider);
145         }
146         pgmX10DevicesStatus = new Boolean[panelSettings.getNbPGMX10Devices()];
147         updatedZoneNames = new HashMap<>();
148         updatedZoneInfos = new HashMap<>();
149         activeAlertList = new ArrayList<>();
150         activeAlertQueue = new ArrayList<>();
151
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
155         // false.
156
157         this.ringing.setValue(false);
158     }
159
160     /**
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
164      * calls.
165      *
166      * @param zone the index of the zone (first zone is index 1)
167      * @return the zone state object (or a dummy zone state)
168      */
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);
173         } else {
174             return zones[zone - 1];
175         }
176     }
177
178     /**
179      * Get the status of a PGM or X10 device
180      *
181      * @param device the index of the PGM/X10 device (0 s for PGM; for X10 device is index 1)
182      *
183      * @return the status (true or false)
184      */
185     public @Nullable Boolean getPGMX10DeviceStatus(int device) {
186         return ((device < 0) || (device >= pgmX10DevicesStatus.length)) ? null : pgmX10DevicesStatus[device];
187     }
188
189     /**
190      * Set the status of a PGM or X10 device
191      *
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
194      */
195     public void setPGMX10DeviceStatus(int device, @Nullable Boolean status) {
196         if ((device >= 0) && (device < pgmX10DevicesStatus.length)) {
197             this.pgmX10DevicesStatus[device] = status;
198         }
199     }
200
201     /**
202      * Get the raw buffer containing all the settings
203      *
204      * @return the raw buffer as a table of bytes
205      */
206     public byte @Nullable [] getUpdateSettings() {
207         return updateSettings;
208     }
209
210     /**
211      * Set the raw buffer containing all the settings
212      *
213      * @param updateSettings the raw buffer as a table of bytes
214      */
215     public void setUpdateSettings(byte[] updateSettings) {
216         this.updateSettings = updateSettings;
217     }
218
219     /**
220      * Get the number of entries in the event log
221      *
222      * @return the number of entries
223      */
224     public int getEventLogSize() {
225         String @Nullable [] localEventLog = eventLog;
226         return (localEventLog == null) ? 0 : localEventLog.length;
227     }
228
229     /**
230      * Set the number of entries in the event log
231      *
232      * @param size the number of entries
233      */
234     public void setEventLogSize(int size) {
235         eventLog = new String[size];
236     }
237
238     /**
239      * Get one entry from the event logs
240      *
241      * @param index the entry index (1 for the most recent entry)
242      *
243      * @return the entry value (event)
244      */
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];
249     }
250
251     /**
252      * Set one entry from the event logs
253      *
254      * @param index the entry index (1 for the most recent entry)
255      * @param event the entry value (event)
256      */
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;
261         }
262     }
263
264     public Map<Integer, Byte> getUpdatedZoneNames() {
265         return updatedZoneNames;
266     }
267
268     public void updateZoneName(int zoneIdx, byte zoneNameIdx) {
269         this.updatedZoneNames.put(zoneIdx, zoneNameIdx);
270     }
271
272     public Map<Integer, Integer> getUpdatedZoneInfos() {
273         return updatedZoneInfos;
274     }
275
276     public void updateZoneInfo(int zoneIdx, int zoneInfo) {
277         this.updatedZoneInfos.put(zoneIdx, zoneInfo);
278     }
279
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.
286
287     public boolean hasActiveAlertsQueued() {
288         return !activeAlertQueue.isEmpty();
289     }
290
291     public String getActiveAlerts() {
292         if (activeAlertList.isEmpty()) {
293             return "None";
294         }
295
296         List<String> alerts = new ArrayList<>();
297
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));
302
303             alerts.add(alert);
304         });
305
306         return String.join(", ", alerts);
307     }
308
309     public void addActiveAlert(int zoneIdx, int code) {
310         PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.ADD, zoneIdx, code);
311         activeAlertQueue.add(alert);
312     }
313
314     public void clearActiveAlert(int zoneIdx, int code) {
315         PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR, zoneIdx, code);
316         activeAlertQueue.add(alert);
317     }
318
319     public void clearAllActiveAlerts() {
320         PowermaxActiveAlert alert = new PowermaxActiveAlert(PowermaxAlertAction.CLEAR_ALL, 0, 0);
321         activeAlertQueue.add(alert);
322     }
323
324     public void resolveActiveAlerts(@Nullable PowermaxState previousState) {
325         copyActiveAlertsFrom(previousState);
326
327         activeAlertQueue.forEach(alert -> {
328             if (alert.action == PowermaxAlertAction.CLEAR_ALL) {
329                 activeAlertList.clear();
330             } else {
331                 activeAlertList.removeIf(e -> e.zone == alert.zone && e.code == alert.code);
332
333                 if (alert.action == PowermaxAlertAction.ADD) {
334                     activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
335                 }
336             }
337         });
338     }
339
340     private void copyActiveAlertsFrom(@Nullable PowermaxState state) {
341         activeAlertList = new ArrayList<>();
342
343         if (state != null) {
344             state.activeAlertList.forEach(alert -> {
345                 activeAlertList.add(new PowermaxActiveAlert(null, alert.zone, alert.code));
346             });
347         }
348     }
349
350     /**
351      * Get the panel mode
352      *
353      * @return either Download or Powerlink or Standard
354      */
355     public @Nullable String getPanelMode() {
356         String mode = null;
357         if (Boolean.TRUE.equals(downloadMode.getValue())) {
358             mode = "Download";
359         } else if (Boolean.TRUE.equals(powerlinkMode.getValue())) {
360             mode = "Powerlink";
361         } else if (Boolean.FALSE.equals(powerlinkMode.getValue())) {
362             mode = "Standard";
363         }
364         return mode;
365     }
366
367     /**
368      * Get whether or not the current arming mode is considered as armed
369      *
370      * @return true or false
371      */
372     public @Nullable Boolean isArmed() {
373         return isArmed(armMode.getValue());
374     }
375
376     /**
377      * Get whether or not an arming mode is considered as armed
378      *
379      * @param armMode the arming mode
380      *
381      * @return true or false; null if mode is unexpected
382      */
383     private static @Nullable Boolean isArmed(@Nullable String armMode) {
384         Boolean result = null;
385         if (armMode != null) {
386             try {
387                 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
388                 result = mode.isArmed();
389             } catch (IllegalArgumentException e) {
390                 result = Boolean.FALSE;
391             }
392         }
393         return result;
394     }
395
396     /**
397      * Get the short description associated to the current arming mode
398      *
399      * @return the short description
400      */
401     public @Nullable String getShortArmMode() {
402         return getShortArmMode(armMode.getValue());
403     }
404
405     /**
406      * Get the short name associated to an arming mode
407      *
408      * @param armMode the arming mode
409      *
410      * @return the short name or null if mode is unexpected
411      */
412     private static @Nullable String getShortArmMode(@Nullable String armMode) {
413         String result = null;
414         if (armMode != null) {
415             try {
416                 PowermaxArmMode mode = PowermaxArmMode.fromName(armMode);
417                 result = mode.getShortName();
418             } catch (IllegalArgumentException e) {
419                 result = armMode;
420             }
421         }
422         return result;
423     }
424
425     /**
426      * Keep only data that are different from another state and reset all others data to undefined
427      *
428      * @param otherState the other state
429      */
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);
434
435             for (int i = 0; i < thisZone.getValues().size(); i++) {
436                 Value<?> thisValue = thisZone.getValues().get(i);
437                 Value<?> otherValue = otherZone.getValues().get(i);
438
439                 if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
440                     thisValue.setValue(null);
441                 }
442             }
443         }
444
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);
449             }
450         }
451
452         for (int i = 0; i < getValues().size(); i++) {
453             Value<?> thisValue = getValues().get(i);
454             Value<?> otherValue = otherState.getValues().get(i);
455
456             if ((thisValue.getValue() != null) && thisValue.getValue().equals(otherValue.getValue())) {
457                 thisValue.setValue(null);
458             }
459         }
460
461         if (hasActiveAlertsQueued()) {
462             resolveActiveAlerts(otherState);
463         }
464     }
465
466     /**
467      * Update (override) the current state data from another state, ignoring in this other state
468      * the undefined data
469      *
470      * @param update the other state to consider for the update
471      */
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);
476
477             for (int i = 0; i < thisZone.getValues().size(); i++) {
478                 Value<?> thisValue = thisZone.getValues().get(i);
479                 Value<?> otherValue = otherZone.getValues().get(i);
480
481                 if (otherValue.getValue() != null) {
482                     thisValue.setValueUnsafe(otherValue.getValue());
483                 }
484             }
485         }
486
487         for (int i = 0; i < pgmX10DevicesStatus.length; i++) {
488             Boolean status = update.getPGMX10DeviceStatus(i);
489             if (status != null) {
490                 setPGMX10DeviceStatus(i, status);
491             }
492         }
493
494         for (int i = 0; i < getValues().size(); i++) {
495             Value<?> thisValue = getValues().get(i);
496             Value<?> otherValue = update.getValues().get(i);
497
498             if (otherValue.getValue() != null) {
499                 thisValue.setValueUnsafe(otherValue.getValue());
500             }
501         }
502
503         if (update.getEventLogSize() > getEventLogSize()) {
504             setEventLogSize(update.getEventLogSize());
505         }
506         for (int i = 1; i <= getEventLogSize(); i++) {
507             String log = update.getEventLog(i);
508             if (log != null) {
509                 setEventLog(i, log);
510             }
511         }
512
513         if (update.hasActiveAlertsQueued()) {
514             copyActiveAlertsFrom(update);
515         }
516     }
517
518     @Override
519     public String toString() {
520         String str = "Bridge state:";
521
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();
527
528                 str += "\n - " + channel + " = " + vStr;
529                 if (!vStr.equals(state)) {
530                     str += " (" + state + ")";
531                 }
532             }
533         }
534
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");
540             }
541         }
542
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();
549
550                     str += String.format("\n - sensor zone %d %s = %s", i, channel, vStr);
551                     if (!vStr.equals(state)) {
552                         str += " (" + state + ")";
553                     }
554                 }
555             }
556         }
557
558         for (int i = 1; i <= getEventLogSize(); i++) {
559             String log = getEventLog(i);
560             if (log != null) {
561                 str += "\n - event log " + i + " = " + log;
562             }
563         }
564
565         str += "\n - active alarms/alerts = " + getActiveAlerts();
566
567         return str;
568     }
569 }