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.Collection;
18 import java.util.Collections;
19 import java.util.EventObject;
20 import java.util.List;
22 import java.util.concurrent.CopyOnWriteArrayList;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
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.PowermaxStateEvent;
36 import org.openhab.binding.powermax.internal.state.PowermaxStateEventListener;
37 import org.openhab.core.io.transport.serial.SerialPortManager;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.StringType;
40 import org.openhab.core.thing.Bridge;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseBridgeHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
53 * The {@link PowermaxBridgeHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Laurent Garnier - Initial contribution
58 public class PowermaxBridgeHandler extends BaseBridgeHandler implements PowermaxStateEventListener {
60 private final Logger logger = LoggerFactory.getLogger(PowermaxBridgeHandler.class);
62 private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
64 /** Default delay in milliseconds to reset a motion detection */
65 private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3);
67 private static final int NB_EVENT_LOG = 10;
69 private static final PowermaxPanelType DEFAULT_PANEL_TYPE = PowermaxPanelType.POWERMAX_PRO;
71 private static final int JOB_REPEAT = 20;
73 private static final int MAX_DOWNLOAD_ATTEMPTS = 3;
75 private ScheduledFuture<?> globalJob;
77 private List<PowermaxPanelSettingsListener> listeners = new CopyOnWriteArrayList<>();
79 /** The delay in milliseconds to reset a motion detection */
80 private long motionOffDelay;
82 /** The PIN code to use for arming/disarming the Powermax alarm system from openHAB */
83 private String pinCode;
85 /** Force the standard mode rather than trying using the Powerlink mode */
86 private boolean forceStandardMode;
88 /** The object to store the current state of the Powermax alarm system */
89 private PowermaxState currentState;
91 /** The object in charge of the communication with the Powermax alarm system */
92 private PowermaxCommManager commManager;
94 private int remainingDownloadAttempts;
95 private SerialPortManager serialPortManager;
97 public PowermaxBridgeHandler(Bridge thing, SerialPortManager serialPortManager) {
99 this.serialPortManager = serialPortManager;
103 public Collection<Class<? extends ThingHandlerService>> getServices() {
104 return Collections.singleton(PowermaxDiscoveryService.class);
107 public PowermaxState getCurrentState() {
111 public PowermaxPanelSettings getPanelSettings() {
112 return (commManager == null) ? null : commManager.getPanelSettings();
116 public void initialize() {
117 logger.debug("initializing handler for thing {}", getThing().getUID());
121 String threadName = "OH-binding-" + getThing().getUID().getAsString();
123 String errorMsg = null;
124 if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_SERIAL)) {
125 errorMsg = initializeBridgeSerial(getConfigAs(PowermaxSerialConfiguration.class), threadName);
126 } else if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_IP)) {
127 errorMsg = initializeBridgeIp(getConfigAs(PowermaxIpConfiguration.class), threadName);
129 errorMsg = "Unexpected thing type " + getThing().getThingTypeUID();
132 if (errorMsg == null) {
133 if (globalJob == null || globalJob.isCancelled()) {
134 // Delay the startup in case the handler is restarted immediately
135 globalJob = scheduler.scheduleWithFixedDelay(() -> {
137 logger.debug("Powermax job...");
138 updateMotionSensorState();
141 commManager.retryDownloadSetup(remainingDownloadAttempts);
145 } catch (Exception e) {
146 logger.warn("Exception in scheduled job: {}", e.getMessage(), e);
148 }, 10, JOB_REPEAT, TimeUnit.SECONDS);
151 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
155 private String initializeBridgeSerial(PowermaxSerialConfiguration config, String threadName) {
156 String errorMsg = null;
157 if (config.serialPort != null && !config.serialPort.trim().isEmpty()
158 && !config.serialPort.trim().startsWith("rfc2217")) {
159 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
160 boolean allowArming = getBooleanSetting(config.allowArming, false);
161 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
162 pinCode = config.pinCode;
163 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
164 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
165 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
167 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
168 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
169 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
170 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
171 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
172 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
173 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
175 commManager = new PowermaxCommManager(config.serialPort, panelType, forceStandardMode, autoSyncTime,
176 serialPortManager, threadName);
178 if (config.serialPort != null && config.serialPort.trim().startsWith("rfc2217")) {
179 errorMsg = "Please use the IP Connection thing type for a serial over IP connection.";
181 errorMsg = "serialPort setting must be defined in thing configuration";
187 private String initializeBridgeIp(PowermaxIpConfiguration config, String threadName) {
188 String errorMsg = null;
189 if (config.ip != null && !config.ip.trim().isEmpty() && config.tcpPort != null) {
190 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
191 boolean allowArming = getBooleanSetting(config.allowArming, false);
192 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
193 pinCode = config.pinCode;
194 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
195 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
196 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
198 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
199 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
200 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
201 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
202 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
203 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
204 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
206 commManager = new PowermaxCommManager(config.ip, config.tcpPort, panelType, forceStandardMode, autoSyncTime,
209 errorMsg = "ip and port settings must be defined in thing configuration";
215 public void dispose() {
216 logger.debug("Handler disposed for thing {}", getThing().getUID());
217 if (globalJob != null && !globalJob.isCancelled()) {
218 globalJob.cancel(true);
227 * Set the state of items linked to motion sensors to OFF when the last trip is older
228 * than the value defined by the variable motionOffDelay
230 private void updateMotionSensorState() {
231 long now = System.currentTimeMillis();
232 if (currentState != null) {
233 boolean update = false;
234 PowermaxState updateState = commManager.createNewState();
235 PowermaxPanelSettings panelSettings = getPanelSettings();
236 for (int i = 1; i <= panelSettings.getNbZones(); i++) {
237 if (panelSettings.getZoneSettings(i) != null && panelSettings.getZoneSettings(i).isMotionSensor()
238 && currentState.isLastTripBeforeTime(i, now - motionOffDelay)) {
240 updateState.setSensorTripped(i, false);
244 updateChannelsFromAlarmState(TRIPPED, updateState);
245 currentState.merge(updateState);
251 * Check that we receive a keep alive message during the last minute
253 private void checkKeepAlive() {
254 long now = System.currentTimeMillis();
255 if (Boolean.TRUE.equals(currentState.isPowerlinkMode()) && (currentState.getLastKeepAlive() != null)
256 && ((now - currentState.getLastKeepAlive()) > ONE_MINUTE)) {
257 // Let Powermax know we are alive
258 commManager.sendRestoreMessage();
259 currentState.setLastKeepAlive(now);
263 private void tryReconnect() {
264 logger.debug("trying to reconnect...");
266 currentState = commManager.createNewState();
267 if (openConnection()) {
268 updateStatus(ThingStatus.ONLINE);
269 if (forceStandardMode) {
270 currentState.setPowerlinkMode(false);
271 updateChannelsFromAlarmState(MODE, currentState);
272 processPanelSettings();
274 commManager.startDownload();
277 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reconnection failed");
282 * Open a TCP or Serial connection to the Powermax Alarm Panel
284 * @return true if the connection has been opened
286 private synchronized boolean openConnection() {
287 if (commManager != null) {
288 commManager.addEventListener(this);
291 remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS;
292 logger.debug("openConnection(): {}", isConnected() ? "connected" : "disconnected");
293 return isConnected();
297 * Close TCP or Serial connection to the Powermax Alarm Panel and remove the Event Listener
299 private synchronized void closeConnection() {
300 if (commManager != null) {
302 commManager.removeEventListener(this);
304 logger.debug("closeConnection(): disconnected");
307 private boolean isConnected() {
308 return commManager == null ? false : commManager.isConnected();
312 public void handleCommand(ChannelUID channelUID, Command command) {
313 logger.debug("Received command {} from channel {}", command, channelUID.getId());
315 if (command instanceof RefreshType) {
316 updateChannelsFromAlarmState(channelUID.getId(), currentState);
318 switch (channelUID.getId()) {
321 PowermaxArmMode armMode = PowermaxArmMode.fromShortName(command.toString());
323 } catch (IllegalArgumentException e) {
324 logger.debug("Powermax alarm binding: invalid command {}", command);
328 if (command instanceof OnOffType) {
330 command.equals(OnOffType.ON) ? PowermaxArmMode.ARMED_AWAY : PowermaxArmMode.DISARMED);
332 logger.debug("Command of type {} while OnOffType is expected. Command is ignored.",
333 command.getClass().getSimpleName());
339 case UPDATE_EVENT_LOGS:
346 logger.debug("No available command for channel {}. Command is ignored.", channelUID.getId());
352 private void armCommand(PowermaxArmMode armMode) {
353 if (!isConnected()) {
354 logger.debug("Powermax alarm binding not connected. Arm command is ignored.");
356 commManager.requestArmMode(armMode,
357 currentState.isPowerlinkMode() ? getPanelSettings().getFirstPinCode() : pinCode);
361 private void pgmCommand(Command command) {
362 if (!isConnected()) {
363 logger.debug("Powermax alarm binding not connected. PGM command is ignored.");
365 commManager.sendPGMX10(command, null);
369 public void x10Command(Byte deviceNr, Command command) {
370 if (!isConnected()) {
371 logger.debug("Powermax alarm binding not connected. X10 command is ignored.");
373 commManager.sendPGMX10(command, deviceNr);
377 public void zoneBypassed(byte zoneNr, boolean bypassed) {
378 if (!isConnected()) {
379 logger.debug("Powermax alarm binding not connected. Zone bypass command is ignored.");
380 } else if (!Boolean.TRUE.equals(currentState.isPowerlinkMode())) {
381 logger.debug("Powermax alarm binding: Bypass option only supported in Powerlink mode");
382 } else if (!getPanelSettings().isBypassEnabled()) {
383 logger.debug("Powermax alarm binding: Bypass option not enabled in panel settings");
385 commManager.sendZoneBypass(bypassed, zoneNr, getPanelSettings().getFirstPinCode());
389 private void downloadEventLog() {
390 if (!isConnected()) {
391 logger.debug("Powermax alarm binding not connected. Event logs command is ignored.");
394 .requestEventLog(currentState.isPowerlinkMode() ? getPanelSettings().getFirstPinCode() : pinCode);
398 public void downloadSetup() {
399 if (!isConnected()) {
400 logger.debug("Powermax alarm binding not connected. Download setup command is ignored.");
401 } else if (!Boolean.TRUE.equals(currentState.isPowerlinkMode())) {
402 logger.debug("Powermax alarm binding: download setup only supported in Powerlink mode");
403 } else if (commManager.isDownloadRunning()) {
404 logger.debug("Powermax alarm binding: download setup not started as one is in progress");
406 commManager.startDownload();
407 if (currentState.getLastKeepAlive() != null) {
408 currentState.setLastKeepAlive(System.currentTimeMillis());
413 public String getInfoSetup() {
414 return (getPanelSettings() == null) ? "" : getPanelSettings().getInfo();
418 public void onNewStateEvent(EventObject event) {
419 PowermaxStateEvent stateEvent = (PowermaxStateEvent) event;
420 PowermaxState updateState = stateEvent.getState();
422 if (Boolean.TRUE.equals(currentState.isPowerlinkMode())
423 && Boolean.TRUE.equals(updateState.isDownloadSetupRequired())) {
424 // After Enrolling Powerlink or if a reset is required
425 logger.debug("Powermax alarm binding: Reset");
426 commManager.startDownload();
427 if (currentState.getLastKeepAlive() != null) {
428 currentState.setLastKeepAlive(System.currentTimeMillis());
430 } else if (Boolean.FALSE.equals(currentState.isPowerlinkMode()) && updateState.getLastKeepAlive() != null) {
431 // Were are in standard mode but received a keep alive message
432 // so we switch in PowerLink mode
433 logger.debug("Powermax alarm binding: Switching to Powerlink mode");
434 commManager.startDownload();
437 boolean doProcessSettings = (updateState.isPowerlinkMode() != null);
439 for (int i = 1; i <= getPanelSettings().getNbZones(); i++) {
440 if (Boolean.TRUE.equals(updateState.isSensorArmed(i))
441 && Boolean.TRUE.equals(currentState.isSensorBypassed(i))) {
442 updateState.setSensorArmed(i, false);
446 updateState.keepOnlyDifferencesWith(currentState);
447 updateChannelsFromAlarmState(updateState);
448 currentState.merge(updateState);
450 PowermaxPanelSettings panelSettings = getPanelSettings();
451 if (!updateState.getUpdatedZoneNames().isEmpty()) {
452 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
453 if (panelSettings.getZoneSettings(zoneIdx) != null) {
454 for (PowermaxPanelSettingsListener listener : listeners) {
455 listener.onZoneSettingsUpdated(zoneIdx, panelSettings);
461 if (doProcessSettings) {
462 // There is a change of mode (standard or Powerlink)
463 processPanelSettings();
464 commManager.exitDownload();
468 private void processPanelSettings() {
469 if (commManager.processPanelSettings(currentState.isPowerlinkMode())) {
470 for (PowermaxPanelSettingsListener listener : listeners) {
471 listener.onPanelSettingsUpdated(getPanelSettings());
473 remainingDownloadAttempts = 0;
475 logger.info("Powermax alarm binding: setup download failed!");
476 for (PowermaxPanelSettingsListener listener : listeners) {
477 listener.onPanelSettingsUpdated(null);
479 remainingDownloadAttempts--;
481 updatePropertiesFromPanelSettings();
482 if (currentState.isPowerlinkMode()) {
483 logger.debug("Powermax alarm binding: running in Powerlink mode");
484 commManager.sendRestoreMessage();
486 logger.debug("Powermax alarm binding: running in Standard mode");
487 commManager.getInfosWhenInStandardMode();
492 * Update channels to match a new alarm system state
494 * @param state: the alarm system state
496 private void updateChannelsFromAlarmState(PowermaxState state) {
497 updateChannelsFromAlarmState(null, state);
501 * Update channels to match a new alarm system state
503 * @param channel: filter on a particular channel; if null, consider all channels
504 * @param state: the alarm system state
506 private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) {
511 if (((channel == null) || channel.equals(MODE)) && isLinked(MODE) && (state.getPanelMode() != null)) {
512 updateState(MODE, new StringType(state.getPanelMode()));
514 if (((channel == null) || channel.equals(SYSTEM_STATUS)) && isLinked(SYSTEM_STATUS)
515 && (state.getStatusStr() != null)) {
516 updateState(SYSTEM_STATUS, new StringType(state.getStatusStr()));
518 if (((channel == null) || channel.equals(READY)) && isLinked(READY) && (state.isReady() != null)) {
519 updateState(READY, state.isReady() ? OnOffType.ON : OnOffType.OFF);
521 if (((channel == null) || channel.equals(WITH_ZONES_BYPASSED)) && isLinked(WITH_ZONES_BYPASSED)
522 && (state.isBypass() != null)) {
523 updateState(WITH_ZONES_BYPASSED, state.isBypass() ? OnOffType.ON : OnOffType.OFF);
525 if (((channel == null) || channel.equals(ALARM_ACTIVE)) && isLinked(ALARM_ACTIVE)
526 && (state.isAlarmActive() != null)) {
527 updateState(ALARM_ACTIVE, state.isAlarmActive() ? OnOffType.ON : OnOffType.OFF);
529 if (((channel == null) || channel.equals(TROUBLE)) && isLinked(TROUBLE) && (state.isTrouble() != null)) {
530 updateState(TROUBLE, state.isTrouble() ? OnOffType.ON : OnOffType.OFF);
532 if (((channel == null) || channel.equals(ALERT_IN_MEMORY)) && isLinked(ALERT_IN_MEMORY)
533 && (state.isAlertInMemory() != null)) {
534 updateState(ALERT_IN_MEMORY, state.isAlertInMemory() ? OnOffType.ON : OnOffType.OFF);
536 if (((channel == null) || channel.equals(SYSTEM_ARMED)) && isLinked(SYSTEM_ARMED)
537 && (state.isArmed() != null)) {
538 updateState(SYSTEM_ARMED, state.isArmed() ? OnOffType.ON : OnOffType.OFF);
540 if (((channel == null) || channel.equals(ARM_MODE)) && isLinked(ARM_MODE)
541 && (state.getShortArmMode() != null)) {
542 updateState(ARM_MODE, new StringType(state.getShortArmMode()));
544 if (((channel == null) || channel.equals(PGM_STATUS)) && isLinked(PGM_STATUS)
545 && (state.getPGMX10DeviceStatus(0) != null)) {
546 updateState(PGM_STATUS, state.getPGMX10DeviceStatus(0) ? OnOffType.ON : OnOffType.OFF);
548 for (int i = 1; i <= NB_EVENT_LOG; i++) {
549 String channel2 = String.format(EVENT_LOG, i);
550 if (((channel == null) || channel.equals(channel2)) && isLinked(channel2)
551 && (state.getEventLog(i) != null)) {
552 updateState(channel2, new StringType(state.getEventLog(i)));
556 for (Thing thing : getThing().getThings()) {
557 if (thing.getHandler() != null) {
558 PowermaxThingHandler handler = (PowermaxThingHandler) thing.getHandler();
559 if (handler != null) {
560 if (thing.getThingTypeUID().equals(THING_TYPE_ZONE)) {
561 if ((channel == null) || channel.equals(TRIPPED)) {
562 handler.updateChannelFromAlarmState(TRIPPED, state);
564 if ((channel == null) || channel.equals(LAST_TRIP)) {
565 handler.updateChannelFromAlarmState(LAST_TRIP, state);
567 if ((channel == null) || channel.equals(BYPASSED)) {
568 handler.updateChannelFromAlarmState(BYPASSED, state);
570 if ((channel == null) || channel.equals(ARMED)) {
571 handler.updateChannelFromAlarmState(ARMED, state);
573 if ((channel == null) || channel.equals(LOW_BATTERY)) {
574 handler.updateChannelFromAlarmState(LOW_BATTERY, state);
576 } else if (thing.getThingTypeUID().equals(THING_TYPE_X10)) {
577 if ((channel == null) || channel.equals(X10_STATUS)) {
578 handler.updateChannelFromAlarmState(X10_STATUS, state);
587 * Update properties to match the alarm panel settings
589 private void updatePropertiesFromPanelSettings() {
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);
597 value = panelSettings.getPanelSerial();
598 if (value != null && !value.isEmpty()) {
599 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
601 value = panelSettings.getPanelEprom();
602 if (value != null && !value.isEmpty()) {
603 properties.put(Thing.PROPERTY_HARDWARE_VERSION, value);
605 value = panelSettings.getPanelSoftware();
606 if (value != null && !value.isEmpty()) {
607 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
609 updateProperties(properties);
612 public boolean registerPanelSettingsListener(PowermaxPanelSettingsListener listener) {
613 boolean inList = true;
614 if (!listeners.contains(listener)) {
615 inList = listeners.add(listener);
620 public boolean unregisterPanelSettingsListener(PowermaxPanelSettingsListener listener) {
621 return listeners.remove(listener);
624 private boolean getBooleanSetting(Boolean value, boolean defaultValue) {
625 return value != null ? value.booleanValue() : defaultValue;
628 private long getMotionOffDelaySetting(Integer value, long defaultValue) {
629 return value != null ? value.intValue() * ONE_MINUTE : defaultValue;
632 private PowermaxPanelType getPanelTypeSetting(String value, PowermaxPanelType defaultValue) {
633 PowermaxPanelType result;
636 result = PowermaxPanelType.fromLabel(value);
637 } catch (IllegalArgumentException e) {
638 result = defaultValue;
639 logger.debug("Powermax alarm binding: panel type not configured correctly");
642 result = defaultValue;