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