]> git.basschouten.com Git - openhab-addons.git/blob
6ff9b22c9c65e05278678511d3cb5bc168bce6b6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.handler;
14
15 import static org.openhab.binding.powermax.internal.PowermaxBindingConstants.*;
16
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.EventObject;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.concurrent.CopyOnWriteArrayList;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25
26 import org.openhab.binding.powermax.internal.config.PowermaxIpConfiguration;
27 import org.openhab.binding.powermax.internal.config.PowermaxSerialConfiguration;
28 import org.openhab.binding.powermax.internal.discovery.PowermaxDiscoveryService;
29 import org.openhab.binding.powermax.internal.message.PowermaxCommManager;
30 import org.openhab.binding.powermax.internal.message.PowermaxSendType;
31 import org.openhab.binding.powermax.internal.state.PowermaxArmMode;
32 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings;
33 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettingsListener;
34 import org.openhab.binding.powermax.internal.state.PowermaxPanelType;
35 import org.openhab.binding.powermax.internal.state.PowermaxState;
36 import org.openhab.binding.powermax.internal.state.PowermaxStateContainer.Value;
37 import org.openhab.binding.powermax.internal.state.PowermaxStateEvent;
38 import org.openhab.binding.powermax.internal.state.PowermaxStateEventListener;
39 import org.openhab.core.i18n.TimeZoneProvider;
40 import org.openhab.core.io.transport.serial.SerialPortManager;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * The {@link PowermaxBridgeHandler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Laurent Garnier - Initial contribution
61  */
62 public class PowermaxBridgeHandler extends BaseBridgeHandler implements PowermaxStateEventListener {
63
64     private final Logger logger = LoggerFactory.getLogger(PowermaxBridgeHandler.class);
65     private final SerialPortManager serialPortManager;
66     private final TimeZoneProvider timeZoneProvider;
67
68     private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
69     private static final long FIVE_MINUTES = TimeUnit.MINUTES.toMillis(5);
70
71     /** Default delay in milliseconds to reset a motion detection */
72     private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3);
73
74     private static final int NB_EVENT_LOG = 10;
75
76     private static final PowermaxPanelType DEFAULT_PANEL_TYPE = PowermaxPanelType.POWERMAX_PRO;
77
78     private static final int JOB_REPEAT = 20;
79
80     private static final int MAX_DOWNLOAD_ATTEMPTS = 3;
81
82     private ScheduledFuture<?> globalJob;
83
84     private List<PowermaxPanelSettingsListener> listeners = new CopyOnWriteArrayList<>();
85
86     /** The delay in milliseconds to reset a motion detection */
87     private long motionOffDelay;
88
89     /** The PIN code to use for arming/disarming the Powermax alarm system from openHAB */
90     private String pinCode;
91
92     /** Force the standard mode rather than trying using the Powerlink mode */
93     private boolean forceStandardMode;
94
95     /** The object to store the current state of the Powermax alarm system */
96     private PowermaxState currentState;
97
98     /** The object in charge of the communication with the Powermax alarm system */
99     private PowermaxCommManager commManager;
100
101     private int remainingDownloadAttempts;
102
103     public PowermaxBridgeHandler(Bridge thing, SerialPortManager serialPortManager, TimeZoneProvider timeZoneProvider) {
104         super(thing);
105         this.serialPortManager = serialPortManager;
106         this.timeZoneProvider = timeZoneProvider;
107     }
108
109     @Override
110     public Collection<Class<? extends ThingHandlerService>> getServices() {
111         return Collections.singleton(PowermaxDiscoveryService.class);
112     }
113
114     public PowermaxState getCurrentState() {
115         return currentState;
116     }
117
118     public PowermaxPanelSettings getPanelSettings() {
119         return (commManager == null) ? null : commManager.getPanelSettings();
120     }
121
122     @Override
123     public void initialize() {
124         logger.debug("initializing handler for thing {}", getThing().getUID());
125
126         commManager = null;
127
128         String threadName = "OH-binding-" + getThing().getUID().getAsString();
129
130         String errorMsg = null;
131         if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_SERIAL)) {
132             errorMsg = initializeBridgeSerial(getConfigAs(PowermaxSerialConfiguration.class), threadName);
133         } else if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_IP)) {
134             errorMsg = initializeBridgeIp(getConfigAs(PowermaxIpConfiguration.class), threadName);
135         } else {
136             errorMsg = "Unexpected thing type " + getThing().getThingTypeUID();
137         }
138
139         if (errorMsg == null) {
140             if (globalJob == null || globalJob.isCancelled()) {
141                 // Delay the startup in case the handler is restarted immediately
142                 globalJob = scheduler.scheduleWithFixedDelay(() -> {
143                     try {
144                         logger.trace("Powermax job...");
145                         updateMotionSensorState();
146                         if (isConnected()) {
147                             checkKeepAlive();
148                             commManager.retryDownloadSetup(remainingDownloadAttempts);
149                         } else {
150                             tryReconnect();
151                         }
152                     } catch (Exception e) {
153                         logger.warn("Exception in scheduled job: {}", e.getMessage(), e);
154                     }
155                 }, 10, JOB_REPEAT, TimeUnit.SECONDS);
156             }
157         } else {
158             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
159         }
160     }
161
162     private String initializeBridgeSerial(PowermaxSerialConfiguration config, String threadName) {
163         String errorMsg = null;
164         if (config.serialPort != null && !config.serialPort.trim().isEmpty()
165                 && !config.serialPort.trim().startsWith("rfc2217")) {
166             motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
167             boolean allowArming = getBooleanSetting(config.allowArming, false);
168             boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
169             pinCode = config.pinCode;
170             forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
171             PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
172             boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
173
174             PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
175             PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
176             PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
177             PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
178             PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
179             PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
180             PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
181
182             commManager = new PowermaxCommManager(config.serialPort, panelType, forceStandardMode, autoSyncTime,
183                     serialPortManager, threadName, timeZoneProvider);
184         } else {
185             if (config.serialPort != null && config.serialPort.trim().startsWith("rfc2217")) {
186                 errorMsg = "Please use the IP Connection thing type for a serial over IP connection.";
187             } else {
188                 errorMsg = "serialPort setting must be defined in thing configuration";
189             }
190         }
191         return errorMsg;
192     }
193
194     private String initializeBridgeIp(PowermaxIpConfiguration config, String threadName) {
195         String errorMsg = null;
196         if (config.ip != null && !config.ip.trim().isEmpty() && config.tcpPort != null) {
197             motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
198             boolean allowArming = getBooleanSetting(config.allowArming, false);
199             boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
200             pinCode = config.pinCode;
201             forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
202             PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
203             boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
204
205             PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
206             PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
207             PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
208             PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
209             PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
210             PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
211             PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
212
213             commManager = new PowermaxCommManager(config.ip, config.tcpPort, panelType, forceStandardMode, autoSyncTime,
214                     threadName, timeZoneProvider);
215         } else {
216             errorMsg = "ip and port settings must be defined in thing configuration";
217         }
218         return errorMsg;
219     }
220
221     @Override
222     public void dispose() {
223         logger.debug("Handler disposed for thing {}", getThing().getUID());
224         if (globalJob != null && !globalJob.isCancelled()) {
225             globalJob.cancel(true);
226             globalJob = null;
227         }
228         closeConnection();
229         commManager = null;
230         super.dispose();
231     }
232
233     /*
234      * Set the state of items linked to motion sensors to OFF when the last trip is older
235      * than the value defined by the variable motionOffDelay
236      */
237     private void updateMotionSensorState() {
238         long now = System.currentTimeMillis();
239         if (currentState != null) {
240             boolean update = false;
241             PowermaxState updateState = commManager.createNewState();
242             PowermaxPanelSettings panelSettings = getPanelSettings();
243             for (int i = 1; i <= panelSettings.getNbZones(); i++) {
244                 if (panelSettings.getZoneSettings(i) != null && panelSettings.getZoneSettings(i).isMotionSensor()
245                         && currentState.getZone(i).isLastTripBeforeTime(now - motionOffDelay)) {
246                     update = true;
247                     updateState.getZone(i).tripped.setValue(false);
248                 }
249             }
250             if (update) {
251                 updateChannelsFromAlarmState(TRIPPED, updateState);
252                 currentState.merge(updateState);
253             }
254         }
255     }
256
257     /*
258      * Check that we're actively communicating with the panel
259      */
260     private void checkKeepAlive() {
261         long now = System.currentTimeMillis();
262         if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())
263                 && (currentState.lastKeepAlive.getValue() != null)
264                 && ((now - currentState.lastKeepAlive.getValue()) > ONE_MINUTE)) {
265             // In Powerlink mode: let Powermax know we are alive
266             commManager.sendRestoreMessage();
267             currentState.lastKeepAlive.setValue(now);
268         } else if (!Boolean.TRUE.equals(currentState.downloadMode.getValue())
269                 && (currentState.lastMessageReceived.getValue() != null)
270                 && ((now - currentState.lastMessageReceived.getValue()) > FIVE_MINUTES)) {
271             // In Standard mode: ping the panel every so often to detect disconnects
272             commManager.sendMessage(PowermaxSendType.STATUS);
273         }
274     }
275
276     private void tryReconnect() {
277         logger.info("Trying to connect or reconnect...");
278         closeConnection();
279         currentState = commManager.createNewState();
280         try {
281             openConnection();
282             logger.debug("openConnection(): connected");
283             updateStatus(ThingStatus.ONLINE);
284             if (forceStandardMode) {
285                 currentState.powerlinkMode.setValue(false);
286                 updateChannelsFromAlarmState(MODE, currentState);
287                 processPanelSettings();
288             } else {
289                 commManager.startDownload();
290             }
291         } catch (Exception e) {
292             logger.debug("openConnection(): {}", e.getMessage(), e);
293             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
294             setAllChannelsOffline();
295         }
296     }
297
298     /**
299      * Open a TCP or Serial connection to the Powermax Alarm Panel
300      *
301      * @return true if the connection has been opened
302      */
303     private synchronized void openConnection() throws Exception {
304         if (commManager != null) {
305             commManager.addEventListener(this);
306             commManager.open();
307         }
308         remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS;
309     }
310
311     /**
312      * Close TCP or Serial connection to the Powermax Alarm Panel and remove the Event Listener
313      */
314     private synchronized void closeConnection() {
315         if (commManager != null) {
316             commManager.close();
317             commManager.removeEventListener(this);
318         }
319         logger.debug("closeConnection(): disconnected");
320     }
321
322     private boolean isConnected() {
323         return commManager == null ? false : commManager.isConnected();
324     }
325
326     @Override
327     public void handleCommand(ChannelUID channelUID, Command command) {
328         logger.debug("Received command {} from channel {}", command, channelUID.getId());
329
330         if (command instanceof RefreshType) {
331             updateChannelsFromAlarmState(channelUID.getId(), currentState);
332         } else {
333             switch (channelUID.getId()) {
334                 case ARM_MODE:
335                     try {
336                         PowermaxArmMode armMode = PowermaxArmMode.fromShortName(command.toString());
337                         armCommand(armMode);
338                     } catch (IllegalArgumentException e) {
339                         logger.debug("Powermax alarm binding: invalid command {}", command);
340                     }
341                     break;
342                 case SYSTEM_ARMED:
343                     if (command instanceof OnOffType) {
344                         armCommand(
345                                 command.equals(OnOffType.ON) ? PowermaxArmMode.ARMED_AWAY : PowermaxArmMode.DISARMED);
346                     } else {
347                         logger.debug("Command of type {} while OnOffType is expected. Command is ignored.",
348                                 command.getClass().getSimpleName());
349                     }
350                     break;
351                 case PGM_STATUS:
352                     pgmCommand(command);
353                     break;
354                 case UPDATE_EVENT_LOGS:
355                     downloadEventLog();
356                     break;
357                 case DOWNLOAD_SETUP:
358                     downloadSetup();
359                     break;
360                 default:
361                     logger.debug("No available command for channel {}. Command is ignored.", channelUID.getId());
362                     break;
363             }
364         }
365     }
366
367     private void armCommand(PowermaxArmMode armMode) {
368         if (!isConnected()) {
369             logger.debug("Powermax alarm binding not connected. Arm command is ignored.");
370         } else {
371             commManager.requestArmMode(armMode,
372                     Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
373                             : pinCode);
374         }
375     }
376
377     private void pgmCommand(Command command) {
378         if (!isConnected()) {
379             logger.debug("Powermax alarm binding not connected. PGM command is ignored.");
380         } else {
381             commManager.sendPGMX10(command, null);
382         }
383     }
384
385     public void x10Command(Byte deviceNr, Command command) {
386         if (!isConnected()) {
387             logger.debug("Powermax alarm binding not connected. X10 command is ignored.");
388         } else {
389             commManager.sendPGMX10(command, deviceNr);
390         }
391     }
392
393     public void zoneBypassed(byte zoneNr, boolean bypassed) {
394         if (!isConnected()) {
395             logger.debug("Powermax alarm binding not connected. Zone bypass command is ignored.");
396         } else if (!Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
397             logger.debug("Powermax alarm binding: Bypass option only supported in Powerlink mode");
398         } else if (!getPanelSettings().isBypassEnabled()) {
399             logger.debug("Powermax alarm binding: Bypass option not enabled in panel settings");
400         } else {
401             commManager.sendZoneBypass(bypassed, zoneNr, getPanelSettings().getFirstPinCode());
402         }
403     }
404
405     private void downloadEventLog() {
406         if (!isConnected()) {
407             logger.debug("Powermax alarm binding not connected. Event logs command is ignored.");
408         } else {
409             commManager.requestEventLog(
410                     Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
411                             : pinCode);
412         }
413     }
414
415     public void downloadSetup() {
416         if (!isConnected()) {
417             logger.debug("Powermax alarm binding not connected. Download setup command is ignored.");
418         } else if (!Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
419             logger.debug("Powermax alarm binding: download setup only supported in Powerlink mode");
420         } else if (commManager.isDownloadRunning()) {
421             logger.debug("Powermax alarm binding: download setup not started as one is in progress");
422         } else {
423             commManager.startDownload();
424             if (currentState.lastKeepAlive.getValue() != null) {
425                 currentState.lastKeepAlive.setValue(System.currentTimeMillis());
426             }
427         }
428     }
429
430     public String getInfoSetup() {
431         return (getPanelSettings() == null) ? "" : getPanelSettings().getInfo();
432     }
433
434     @Override
435     public void onNewStateEvent(EventObject event) {
436         PowermaxStateEvent stateEvent = (PowermaxStateEvent) event;
437         PowermaxState updateState = stateEvent.getState();
438
439         if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())
440                 && Boolean.TRUE.equals(updateState.downloadSetupRequired.getValue())) {
441             // After Enrolling Powerlink or if a reset is required
442             logger.debug("Powermax alarm binding: Reset");
443             commManager.startDownload();
444             updateState.downloadSetupRequired.setValue(false);
445             if (currentState.lastKeepAlive.getValue() != null) {
446                 currentState.lastKeepAlive.setValue(System.currentTimeMillis());
447             }
448         } else if (Boolean.FALSE.equals(currentState.powerlinkMode.getValue())
449                 && updateState.lastKeepAlive.getValue() != null) {
450             // Were are in standard mode but received a keep alive message
451             // so we switch in PowerLink mode
452             logger.debug("Powermax alarm binding: Switching to Powerlink mode");
453             commManager.startDownload();
454         }
455
456         boolean doProcessSettings = (updateState.powerlinkMode.getValue() != null);
457
458         for (int i = 1; i <= getPanelSettings().getNbZones(); i++) {
459             if (Boolean.TRUE.equals(updateState.getZone(i).armed.getValue())
460                     && Boolean.TRUE.equals(currentState.getZone(i).bypassed.getValue())) {
461                 updateState.getZone(i).armed.setValue(false);
462             }
463         }
464
465         updateState.keepOnlyDifferencesWith(currentState);
466         updateChannelsFromAlarmState(updateState);
467         currentState.merge(updateState);
468
469         PowermaxPanelSettings panelSettings = getPanelSettings();
470         if (!updateState.getUpdatedZoneNames().isEmpty()) {
471             for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
472                 if (panelSettings.getZoneSettings(zoneIdx) != null) {
473                     for (PowermaxPanelSettingsListener listener : listeners) {
474                         listener.onZoneSettingsUpdated(zoneIdx, panelSettings);
475                     }
476                 }
477             }
478         }
479
480         if (doProcessSettings) {
481             // There is a change of mode (standard or Powerlink)
482             processPanelSettings();
483             commManager.exitDownload();
484         }
485     }
486
487     @Override
488     public void onCommunicationFailure(String message) {
489         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
490         setAllChannelsOffline();
491     }
492
493     private void processPanelSettings() {
494         if (commManager.processPanelSettings(Boolean.TRUE.equals(currentState.powerlinkMode.getValue()))) {
495             for (PowermaxPanelSettingsListener listener : listeners) {
496                 listener.onPanelSettingsUpdated(getPanelSettings());
497             }
498             remainingDownloadAttempts = 0;
499         } else {
500             logger.info("Powermax alarm binding: setup download failed!");
501             for (PowermaxPanelSettingsListener listener : listeners) {
502                 listener.onPanelSettingsUpdated(null);
503             }
504             remainingDownloadAttempts--;
505         }
506         updatePropertiesFromPanelSettings();
507         if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
508             logger.info("Powermax alarm binding: running in Powerlink mode");
509             commManager.sendRestoreMessage();
510         } else {
511             logger.info("Powermax alarm binding: running in Standard mode");
512             commManager.getInfosWhenInStandardMode();
513         }
514     }
515
516     /**
517      * Update channels to match a new alarm system state
518      *
519      * @param state: the alarm system state
520      */
521     private void updateChannelsFromAlarmState(PowermaxState state) {
522         updateChannelsFromAlarmState(null, state);
523     }
524
525     /**
526      * Update channels to match a new alarm system state
527      *
528      * @param channel: filter on a particular channel; if null, consider all channels
529      * @param state: the alarm system state
530      */
531     private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) {
532         if (state == null || !isConnected()) {
533             return;
534         }
535
536         for (Value<?> value : state.getValues()) {
537             String vChannel = value.getChannel();
538
539             if (((channel == null) || channel.equals(vChannel)) && (vChannel != null) && isLinked(vChannel)
540                     && (value.getValue() != null)) {
541                 updateState(vChannel, value.getState());
542             }
543         }
544
545         for (int i = 1; i <= NB_EVENT_LOG; i++) {
546             String channel2 = String.format(EVENT_LOG, i);
547             if (((channel == null) || channel.equals(channel2)) && isLinked(channel2)
548                     && (state.getEventLog(i) != null)) {
549                 updateState(channel2, new StringType(state.getEventLog(i)));
550             }
551         }
552
553         for (Thing thing : getThing().getThings()) {
554             if (thing.getHandler() != null) {
555                 PowermaxThingHandler handler = (PowermaxThingHandler) thing.getHandler();
556                 if (handler != null) {
557                     if (thing.getThingTypeUID().equals(THING_TYPE_ZONE)) {
558
559                         // All of the zone state objects will have the same list of values.
560                         // The use of getZone(1) here is just to get any PowermaxZoneState
561                         // and use it to get the list of zone channels.
562
563                         for (Value<?> value : state.getZone(1).getValues()) {
564                             String channelId = value.getChannel();
565                             if ((channelId != null) && ((channel == null) || channel.equals(channelId))) {
566                                 handler.updateChannelFromAlarmState(channelId, state);
567                             }
568                         }
569                     } else if (thing.getThingTypeUID().equals(THING_TYPE_X10)) {
570                         if ((channel == null) || channel.equals(X10_STATUS)) {
571                             handler.updateChannelFromAlarmState(X10_STATUS, state);
572                         }
573                     }
574                 }
575             }
576         }
577     }
578
579     /**
580      * Update all channels to an UNDEF state to indicate that communication with the panel is offline
581      */
582     private synchronized void setAllChannelsOffline() {
583         getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF));
584     }
585
586     /**
587      * Update properties to match the alarm panel settings
588      */
589     private void updatePropertiesFromPanelSettings() {
590         String value;
591         Map<String, String> properties = editProperties();
592         PowermaxPanelSettings panelSettings = getPanelSettings();
593         value = (panelSettings.getPanelType() != null) ? panelSettings.getPanelType().getLabel() : null;
594         if (value != null && !value.isEmpty()) {
595             properties.put(Thing.PROPERTY_MODEL_ID, value);
596         }
597         value = panelSettings.getPanelSerial();
598         if (value != null && !value.isEmpty()) {
599             properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
600         }
601         value = panelSettings.getPanelEprom();
602         if (value != null && !value.isEmpty()) {
603             properties.put(Thing.PROPERTY_HARDWARE_VERSION, value);
604         }
605         value = panelSettings.getPanelSoftware();
606         if (value != null && !value.isEmpty()) {
607             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
608         }
609         updateProperties(properties);
610     }
611
612     public boolean registerPanelSettingsListener(PowermaxPanelSettingsListener listener) {
613         boolean inList = true;
614         if (!listeners.contains(listener)) {
615             inList = listeners.add(listener);
616         }
617         return inList;
618     }
619
620     public boolean unregisterPanelSettingsListener(PowermaxPanelSettingsListener listener) {
621         return listeners.remove(listener);
622     }
623
624     private boolean getBooleanSetting(Boolean value, boolean defaultValue) {
625         return value != null ? value.booleanValue() : defaultValue;
626     }
627
628     private long getMotionOffDelaySetting(Integer value, long defaultValue) {
629         return value != null ? value.intValue() * ONE_MINUTE : defaultValue;
630     }
631
632     private PowermaxPanelType getPanelTypeSetting(String value, PowermaxPanelType defaultValue) {
633         PowermaxPanelType result;
634         if (value != null) {
635             try {
636                 result = PowermaxPanelType.fromLabel(value);
637             } catch (IllegalArgumentException e) {
638                 result = defaultValue;
639                 logger.debug("Powermax alarm binding: panel type not configured correctly");
640             }
641         } else {
642             result = defaultValue;
643         }
644         return result;
645     }
646 }