2 * Copyright (c) 2010-2020 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.handler;
15 import static org.openhab.binding.powermax.internal.PowermaxBindingConstants.*;
17 import java.util.EventObject;
18 import java.util.List;
20 import java.util.concurrent.CopyOnWriteArrayList;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.openhab.binding.powermax.internal.config.PowermaxIpConfiguration;
25 import org.openhab.binding.powermax.internal.config.PowermaxSerialConfiguration;
26 import org.openhab.binding.powermax.internal.message.PowermaxCommManager;
27 import org.openhab.binding.powermax.internal.state.PowermaxArmMode;
28 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings;
29 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettingsListener;
30 import org.openhab.binding.powermax.internal.state.PowermaxPanelType;
31 import org.openhab.binding.powermax.internal.state.PowermaxState;
32 import org.openhab.binding.powermax.internal.state.PowermaxStateEvent;
33 import org.openhab.binding.powermax.internal.state.PowermaxStateEventListener;
34 import org.openhab.core.io.transport.serial.SerialPortManager;
35 import org.openhab.core.library.types.OnOffType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseBridgeHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
49 * The {@link PowermaxBridgeHandler} is responsible for handling commands, which are
50 * sent to one of the channels.
52 * @author Laurent Garnier - Initial contribution
54 public class PowermaxBridgeHandler extends BaseBridgeHandler implements PowermaxStateEventListener {
56 private final Logger logger = LoggerFactory.getLogger(PowermaxBridgeHandler.class);
58 private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
60 /** Default delay in milliseconds to reset a motion detection */
61 private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3);
63 private static final int NB_EVENT_LOG = 10;
65 private static final PowermaxPanelType DEFAULT_PANEL_TYPE = PowermaxPanelType.POWERMAX_PRO;
67 private static final int JOB_REPEAT = 20;
69 private static final int MAX_DOWNLOAD_ATTEMPTS = 3;
71 private ScheduledFuture<?> globalJob;
73 private List<PowermaxPanelSettingsListener> listeners = new CopyOnWriteArrayList<>();
75 /** The delay in milliseconds to reset a motion detection */
76 private long motionOffDelay;
78 /** The PIN code to use for arming/disarming the Powermax alarm system from openHAB */
79 private String pinCode;
81 /** Force the standard mode rather than trying using the Powerlink mode */
82 private boolean forceStandardMode;
84 /** The object to store the current state of the Powermax alarm system */
85 private PowermaxState currentState;
87 /** The object in charge of the communication with the Powermax alarm system */
88 private PowermaxCommManager commManager;
90 private int remainingDownloadAttempts;
91 private SerialPortManager serialPortManager;
93 public PowermaxBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
95 this.serialPortManager = serialPortManager;
98 public PowermaxState getCurrentState() {
102 public PowermaxPanelSettings getPanelSettings() {
103 return (commManager == null) ? null : commManager.getPanelSettings();
107 public void initialize() {
108 logger.debug("initializing handler for thing {}", getThing().getUID());
112 String threadName = "OH-binding-" + getThing().getUID().getAsString();
114 String errorMsg = null;
115 if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_SERIAL)) {
116 errorMsg = initializeBridgeSerial(getConfigAs(PowermaxSerialConfiguration.class), threadName);
117 } else if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_IP)) {
118 errorMsg = initializeBridgeIp(getConfigAs(PowermaxIpConfiguration.class), threadName);
120 errorMsg = "Unexpected thing type " + getThing().getThingTypeUID();
123 if (errorMsg == null) {
124 if (globalJob == null || globalJob.isCancelled()) {
125 // Delay the startup in case the handler is restarted immediately
126 globalJob = scheduler.scheduleWithFixedDelay(() -> {
128 logger.debug("Powermax job...");
129 updateMotionSensorState();
132 commManager.retryDownloadSetup(remainingDownloadAttempts);
136 } catch (Exception e) {
137 logger.warn("Exception in scheduled job: {}", e.getMessage(), e);
139 }, 10, JOB_REPEAT, TimeUnit.SECONDS);
142 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
146 private String initializeBridgeSerial(PowermaxSerialConfiguration config, String threadName) {
147 String errorMsg = null;
148 if (config.serialPort != null && !config.serialPort.trim().isEmpty()
149 && !config.serialPort.trim().startsWith("rfc2217")) {
150 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
151 boolean allowArming = getBooleanSetting(config.allowArming, false);
152 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
153 pinCode = config.pinCode;
154 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
155 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
156 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
158 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
159 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
160 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
161 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
162 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
163 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
164 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
166 commManager = new PowermaxCommManager(config.serialPort, panelType, forceStandardMode, autoSyncTime,
167 serialPortManager, threadName);
169 if (config.serialPort != null && config.serialPort.trim().startsWith("rfc2217")) {
170 errorMsg = "Please use the IP Connection thing type for a serial over IP connection.";
172 errorMsg = "serialPort setting must be defined in thing configuration";
178 private String initializeBridgeIp(PowermaxIpConfiguration config, String threadName) {
179 String errorMsg = null;
180 if (config.ip != null && !config.ip.trim().isEmpty() && config.tcpPort != null) {
181 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
182 boolean allowArming = getBooleanSetting(config.allowArming, false);
183 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
184 pinCode = config.pinCode;
185 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
186 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
187 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
189 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
190 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
191 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
192 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
193 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
194 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
195 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
197 commManager = new PowermaxCommManager(config.ip, config.tcpPort, panelType, forceStandardMode, autoSyncTime,
200 errorMsg = "ip and port settings must be defined in thing configuration";
206 public void dispose() {
207 logger.debug("Handler disposed for thing {}", getThing().getUID());
208 if (globalJob != null && !globalJob.isCancelled()) {
209 globalJob.cancel(true);
218 * Set the state of items linked to motion sensors to OFF when the last trip is older
219 * than the value defined by the variable motionOffDelay
221 private void updateMotionSensorState() {
222 long now = System.currentTimeMillis();
223 if (currentState != null) {
224 boolean update = false;
225 PowermaxState updateState = commManager.createNewState();
226 PowermaxPanelSettings panelSettings = getPanelSettings();
227 for (int i = 1; i <= panelSettings.getNbZones(); i++) {
228 if (panelSettings.getZoneSettings(i) != null && panelSettings.getZoneSettings(i).isMotionSensor()
229 && currentState.isLastTripBeforeTime(i, now - motionOffDelay)) {
231 updateState.setSensorTripped(i, false);
235 updateChannelsFromAlarmState(TRIPPED, updateState);
236 currentState.merge(updateState);
242 * Check that we receive a keep alive message during the last minute
244 private void checkKeepAlive() {
245 long now = System.currentTimeMillis();
246 if (Boolean.TRUE.equals(currentState.isPowerlinkMode()) && (currentState.getLastKeepAlive() != null)
247 && ((now - currentState.getLastKeepAlive()) > ONE_MINUTE)) {
248 // Let Powermax know we are alive
249 commManager.sendRestoreMessage();
250 currentState.setLastKeepAlive(now);
254 private void tryReconnect() {
255 logger.debug("trying to reconnect...");
257 currentState = commManager.createNewState();
258 if (openConnection()) {
259 updateStatus(ThingStatus.ONLINE);
260 if (forceStandardMode) {
261 currentState.setPowerlinkMode(false);
262 updateChannelsFromAlarmState(MODE, currentState);
263 processPanelSettings();
265 commManager.startDownload();
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reconnection failed");
273 * Open a TCP or Serial connection to the Powermax Alarm Panel
275 * @return true if the connection has been opened
277 private synchronized boolean openConnection() {
278 if (commManager != null) {
279 commManager.addEventListener(this);
282 remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS;
283 logger.debug("openConnection(): {}", isConnected() ? "connected" : "disconnected");
284 return isConnected();
288 * Close TCP or Serial connection to the Powermax Alarm Panel and remove the Event Listener
290 private synchronized void closeConnection() {
291 if (commManager != null) {
293 commManager.removeEventListener(this);
295 logger.debug("closeConnection(): disconnected");
298 private boolean isConnected() {
299 return commManager == null ? false : commManager.isConnected();
303 public void handleCommand(ChannelUID channelUID, Command command) {
304 logger.debug("Received command {} from channel {}", command, channelUID.getId());
306 if (command instanceof RefreshType) {
307 updateChannelsFromAlarmState(channelUID.getId(), currentState);
309 switch (channelUID.getId()) {
312 PowermaxArmMode armMode = PowermaxArmMode.fromShortName(command.toString());
314 } catch (IllegalArgumentException e) {
315 logger.debug("Powermax alarm binding: invalid command {}", command);
319 if (command instanceof OnOffType) {
321 command.equals(OnOffType.ON) ? PowermaxArmMode.ARMED_AWAY : PowermaxArmMode.DISARMED);
323 logger.debug("Command of type {} while OnOffType is expected. Command is ignored.",
324 command.getClass().getSimpleName());
330 case UPDATE_EVENT_LOGS:
337 logger.debug("No available command for channel {}. Command is ignored.", channelUID.getId());
343 private void armCommand(PowermaxArmMode armMode) {
344 if (!isConnected()) {
345 logger.debug("Powermax alarm binding not connected. Arm command is ignored.");
347 commManager.requestArmMode(armMode,
348 currentState.isPowerlinkMode() ? getPanelSettings().getFirstPinCode() : pinCode);
352 private void pgmCommand(Command command) {
353 if (!isConnected()) {
354 logger.debug("Powermax alarm binding not connected. PGM command is ignored.");
356 commManager.sendPGMX10(command, null);
360 public void x10Command(Byte deviceNr, Command command) {
361 if (!isConnected()) {
362 logger.debug("Powermax alarm binding not connected. X10 command is ignored.");
364 commManager.sendPGMX10(command, deviceNr);
368 public void zoneBypassed(byte zoneNr, boolean bypassed) {
369 if (!isConnected()) {
370 logger.debug("Powermax alarm binding not connected. Zone bypass command is ignored.");
371 } else if (!Boolean.TRUE.equals(currentState.isPowerlinkMode())) {
372 logger.debug("Powermax alarm binding: Bypass option only supported in Powerlink mode");
373 } else if (!getPanelSettings().isBypassEnabled()) {
374 logger.debug("Powermax alarm binding: Bypass option not enabled in panel settings");
376 commManager.sendZoneBypass(bypassed, zoneNr, getPanelSettings().getFirstPinCode());
380 private void downloadEventLog() {
381 if (!isConnected()) {
382 logger.debug("Powermax alarm binding not connected. Event logs command is ignored.");
385 .requestEventLog(currentState.isPowerlinkMode() ? getPanelSettings().getFirstPinCode() : pinCode);
389 public void downloadSetup() {
390 if (!isConnected()) {
391 logger.debug("Powermax alarm binding not connected. Download setup command is ignored.");
392 } else if (!Boolean.TRUE.equals(currentState.isPowerlinkMode())) {
393 logger.debug("Powermax alarm binding: download setup only supported in Powerlink mode");
394 } else if (commManager.isDownloadRunning()) {
395 logger.debug("Powermax alarm binding: download setup not started as one is in progress");
397 commManager.startDownload();
398 if (currentState.getLastKeepAlive() != null) {
399 currentState.setLastKeepAlive(System.currentTimeMillis());
404 public String getInfoSetup() {
405 return (getPanelSettings() == null) ? "" : getPanelSettings().getInfo();
409 public void onNewStateEvent(EventObject event) {
410 PowermaxStateEvent stateEvent = (PowermaxStateEvent) event;
411 PowermaxState updateState = stateEvent.getState();
413 if (Boolean.TRUE.equals(currentState.isPowerlinkMode())
414 && Boolean.TRUE.equals(updateState.isDownloadSetupRequired())) {
415 // After Enrolling Powerlink or if a reset is required
416 logger.debug("Powermax alarm binding: Reset");
417 commManager.startDownload();
418 if (currentState.getLastKeepAlive() != null) {
419 currentState.setLastKeepAlive(System.currentTimeMillis());
421 } else if (Boolean.FALSE.equals(currentState.isPowerlinkMode()) && updateState.getLastKeepAlive() != null) {
422 // Were are in standard mode but received a keep alive message
423 // so we switch in PowerLink mode
424 logger.debug("Powermax alarm binding: Switching to Powerlink mode");
425 commManager.startDownload();
428 boolean doProcessSettings = (updateState.isPowerlinkMode() != null);
430 for (int i = 1; i <= getPanelSettings().getNbZones(); i++) {
431 if (Boolean.TRUE.equals(updateState.isSensorArmed(i))
432 && Boolean.TRUE.equals(currentState.isSensorBypassed(i))) {
433 updateState.setSensorArmed(i, false);
437 updateState.keepOnlyDifferencesWith(currentState);
438 updateChannelsFromAlarmState(updateState);
439 currentState.merge(updateState);
441 PowermaxPanelSettings panelSettings = getPanelSettings();
442 if (!updateState.getUpdatedZoneNames().isEmpty()) {
443 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
444 if (panelSettings.getZoneSettings(zoneIdx) != null) {
445 for (PowermaxPanelSettingsListener listener : listeners) {
446 listener.onZoneSettingsUpdated(zoneIdx, panelSettings);
452 if (doProcessSettings) {
453 // There is a change of mode (standard or Powerlink)
454 processPanelSettings();
455 commManager.exitDownload();
459 private void processPanelSettings() {
460 if (commManager.processPanelSettings(currentState.isPowerlinkMode())) {
461 for (PowermaxPanelSettingsListener listener : listeners) {
462 listener.onPanelSettingsUpdated(getPanelSettings());
464 remainingDownloadAttempts = 0;
466 logger.info("Powermax alarm binding: setup download failed!");
467 for (PowermaxPanelSettingsListener listener : listeners) {
468 listener.onPanelSettingsUpdated(null);
470 remainingDownloadAttempts--;
472 updatePropertiesFromPanelSettings();
473 if (currentState.isPowerlinkMode()) {
474 logger.debug("Powermax alarm binding: running in Powerlink mode");
475 commManager.sendRestoreMessage();
477 logger.debug("Powermax alarm binding: running in Standard mode");
478 commManager.getInfosWhenInStandardMode();
483 * Update channels to match a new alarm system state
485 * @param state: the alarm system state
487 private void updateChannelsFromAlarmState(PowermaxState state) {
488 updateChannelsFromAlarmState(null, state);
492 * Update channels to match a new alarm system state
494 * @param channel: filter on a particular channel; if null, consider all channels
495 * @param state: the alarm system state
497 private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) {
502 if (((channel == null) || channel.equals(MODE)) && isLinked(MODE) && (state.getPanelMode() != null)) {
503 updateState(MODE, new StringType(state.getPanelMode()));
505 if (((channel == null) || channel.equals(SYSTEM_STATUS)) && isLinked(SYSTEM_STATUS)
506 && (state.getStatusStr() != null)) {
507 updateState(SYSTEM_STATUS, new StringType(state.getStatusStr()));
509 if (((channel == null) || channel.equals(READY)) && isLinked(READY) && (state.isReady() != null)) {
510 updateState(READY, state.isReady() ? OnOffType.ON : OnOffType.OFF);
512 if (((channel == null) || channel.equals(WITH_ZONES_BYPASSED)) && isLinked(WITH_ZONES_BYPASSED)
513 && (state.isBypass() != null)) {
514 updateState(WITH_ZONES_BYPASSED, state.isBypass() ? OnOffType.ON : OnOffType.OFF);
516 if (((channel == null) || channel.equals(ALARM_ACTIVE)) && isLinked(ALARM_ACTIVE)
517 && (state.isAlarmActive() != null)) {
518 updateState(ALARM_ACTIVE, state.isAlarmActive() ? OnOffType.ON : OnOffType.OFF);
520 if (((channel == null) || channel.equals(TROUBLE)) && isLinked(TROUBLE) && (state.isTrouble() != null)) {
521 updateState(TROUBLE, state.isTrouble() ? OnOffType.ON : OnOffType.OFF);
523 if (((channel == null) || channel.equals(ALERT_IN_MEMORY)) && isLinked(ALERT_IN_MEMORY)
524 && (state.isAlertInMemory() != null)) {
525 updateState(ALERT_IN_MEMORY, state.isAlertInMemory() ? OnOffType.ON : OnOffType.OFF);
527 if (((channel == null) || channel.equals(SYSTEM_ARMED)) && isLinked(SYSTEM_ARMED)
528 && (state.isArmed() != null)) {
529 updateState(SYSTEM_ARMED, state.isArmed() ? OnOffType.ON : OnOffType.OFF);
531 if (((channel == null) || channel.equals(ARM_MODE)) && isLinked(ARM_MODE)
532 && (state.getShortArmMode() != null)) {
533 updateState(ARM_MODE, new StringType(state.getShortArmMode()));
535 if (((channel == null) || channel.equals(PGM_STATUS)) && isLinked(PGM_STATUS)
536 && (state.getPGMX10DeviceStatus(0) != null)) {
537 updateState(PGM_STATUS, state.getPGMX10DeviceStatus(0) ? OnOffType.ON : OnOffType.OFF);
539 for (int i = 1; i <= NB_EVENT_LOG; i++) {
540 String channel2 = String.format(EVENT_LOG, i);
541 if (((channel == null) || channel.equals(channel2)) && isLinked(channel2)
542 && (state.getEventLog(i) != null)) {
543 updateState(channel2, new StringType(state.getEventLog(i)));
547 for (Thing thing : getThing().getThings()) {
548 if (thing.getHandler() != null) {
549 PowermaxThingHandler handler = (PowermaxThingHandler) thing.getHandler();
550 if (handler != null) {
551 if (thing.getThingTypeUID().equals(THING_TYPE_ZONE)) {
552 if ((channel == null) || channel.equals(TRIPPED)) {
553 handler.updateChannelFromAlarmState(TRIPPED, state);
555 if ((channel == null) || channel.equals(LAST_TRIP)) {
556 handler.updateChannelFromAlarmState(LAST_TRIP, state);
558 if ((channel == null) || channel.equals(BYPASSED)) {
559 handler.updateChannelFromAlarmState(BYPASSED, state);
561 if ((channel == null) || channel.equals(ARMED)) {
562 handler.updateChannelFromAlarmState(ARMED, state);
564 if ((channel == null) || channel.equals(LOW_BATTERY)) {
565 handler.updateChannelFromAlarmState(LOW_BATTERY, state);
567 } else if (thing.getThingTypeUID().equals(THING_TYPE_X10)) {
568 if ((channel == null) || channel.equals(X10_STATUS)) {
569 handler.updateChannelFromAlarmState(X10_STATUS, state);
578 * Update properties to match the alarm panel settings
580 private void updatePropertiesFromPanelSettings() {
582 Map<String, String> properties = editProperties();
583 PowermaxPanelSettings panelSettings = getPanelSettings();
584 value = (panelSettings.getPanelType() != null) ? panelSettings.getPanelType().getLabel() : null;
585 if (value != null && !value.isEmpty()) {
586 properties.put(Thing.PROPERTY_MODEL_ID, value);
588 value = panelSettings.getPanelSerial();
589 if (value != null && !value.isEmpty()) {
590 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
592 value = panelSettings.getPanelEprom();
593 if (value != null && !value.isEmpty()) {
594 properties.put(Thing.PROPERTY_HARDWARE_VERSION, value);
596 value = panelSettings.getPanelSoftware();
597 if (value != null && !value.isEmpty()) {
598 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
600 updateProperties(properties);
603 public boolean registerPanelSettingsListener(PowermaxPanelSettingsListener listener) {
604 boolean inList = true;
605 if (!listeners.contains(listener)) {
606 inList = listeners.add(listener);
611 public boolean unregisterPanelSettingsListener(PowermaxPanelSettingsListener listener) {
612 return listeners.remove(listener);
615 private boolean getBooleanSetting(Boolean value, boolean defaultValue) {
616 return value != null ? value.booleanValue() : defaultValue;
619 private long getMotionOffDelaySetting(Integer value, long defaultValue) {
620 return value != null ? value.intValue() * ONE_MINUTE : defaultValue;
623 private PowermaxPanelType getPanelTypeSetting(String value, PowermaxPanelType defaultValue) {
624 PowermaxPanelType result;
627 result = PowermaxPanelType.fromLabel(value);
628 } catch (IllegalArgumentException e) {
629 result = defaultValue;
630 logger.debug("Powermax alarm binding: panel type not configured correctly");
633 result = defaultValue;