2 * Copyright (c) 2010-2021 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.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;
55 * The {@link PowermaxBridgeHandler} is responsible for handling commands, which are
56 * sent to one of the channels.
58 * @author Laurent Garnier - Initial contribution
60 public class PowermaxBridgeHandler extends BaseBridgeHandler implements PowermaxStateEventListener {
62 private final Logger logger = LoggerFactory.getLogger(PowermaxBridgeHandler.class);
63 private final SerialPortManager serialPortManager;
64 private final TimeZoneProvider timeZoneProvider;
66 private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
68 /** Default delay in milliseconds to reset a motion detection */
69 private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3);
71 private static final int NB_EVENT_LOG = 10;
73 private static final PowermaxPanelType DEFAULT_PANEL_TYPE = PowermaxPanelType.POWERMAX_PRO;
75 private static final int JOB_REPEAT = 20;
77 private static final int MAX_DOWNLOAD_ATTEMPTS = 3;
79 private ScheduledFuture<?> globalJob;
81 private List<PowermaxPanelSettingsListener> listeners = new CopyOnWriteArrayList<>();
83 /** The delay in milliseconds to reset a motion detection */
84 private long motionOffDelay;
86 /** The PIN code to use for arming/disarming the Powermax alarm system from openHAB */
87 private String pinCode;
89 /** Force the standard mode rather than trying using the Powerlink mode */
90 private boolean forceStandardMode;
92 /** The object to store the current state of the Powermax alarm system */
93 private PowermaxState currentState;
95 /** The object in charge of the communication with the Powermax alarm system */
96 private PowermaxCommManager commManager;
98 private int remainingDownloadAttempts;
100 public PowermaxBridgeHandler(Bridge thing, SerialPortManager serialPortManager, TimeZoneProvider timeZoneProvider) {
102 this.serialPortManager = serialPortManager;
103 this.timeZoneProvider = timeZoneProvider;
107 public Collection<Class<? extends ThingHandlerService>> getServices() {
108 return Collections.singleton(PowermaxDiscoveryService.class);
111 public PowermaxState getCurrentState() {
115 public PowermaxPanelSettings getPanelSettings() {
116 return (commManager == null) ? null : commManager.getPanelSettings();
120 public void initialize() {
121 logger.debug("initializing handler for thing {}", getThing().getUID());
125 String threadName = "OH-binding-" + getThing().getUID().getAsString();
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);
133 errorMsg = "Unexpected thing type " + getThing().getThingTypeUID();
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(() -> {
141 logger.trace("Powermax job...");
142 updateMotionSensorState();
145 commManager.retryDownloadSetup(remainingDownloadAttempts);
149 } catch (Exception e) {
150 logger.warn("Exception in scheduled job: {}", e.getMessage(), e);
152 }, 10, JOB_REPEAT, TimeUnit.SECONDS);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
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);
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);
179 commManager = new PowermaxCommManager(config.serialPort, panelType, forceStandardMode, autoSyncTime,
180 serialPortManager, threadName, timeZoneProvider);
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.";
185 errorMsg = "serialPort setting must be defined in thing configuration";
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);
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);
210 commManager = new PowermaxCommManager(config.ip, config.tcpPort, panelType, forceStandardMode, autoSyncTime,
211 threadName, timeZoneProvider);
213 errorMsg = "ip and port settings must be defined in thing configuration";
219 public void dispose() {
220 logger.debug("Handler disposed for thing {}", getThing().getUID());
221 if (globalJob != null && !globalJob.isCancelled()) {
222 globalJob.cancel(true);
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
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)) {
244 updateState.getZone(i).tripped.setValue(false);
248 updateChannelsFromAlarmState(TRIPPED, updateState);
249 currentState.merge(updateState);
255 * Check that we receive a keep alive message during the last minute
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);
268 private void tryReconnect() {
269 logger.debug("trying to reconnect...");
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();
279 commManager.startDownload();
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Reconnection failed");
287 * Open a TCP or Serial connection to the Powermax Alarm Panel
289 * @return true if the connection has been opened
291 private synchronized boolean openConnection() {
292 if (commManager != null) {
293 commManager.addEventListener(this);
296 remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS;
297 logger.debug("openConnection(): {}", isConnected() ? "connected" : "disconnected");
298 return isConnected();
302 * Close TCP or Serial connection to the Powermax Alarm Panel and remove the Event Listener
304 private synchronized void closeConnection() {
305 if (commManager != null) {
307 commManager.removeEventListener(this);
309 logger.debug("closeConnection(): disconnected");
312 private boolean isConnected() {
313 return commManager == null ? false : commManager.isConnected();
317 public void handleCommand(ChannelUID channelUID, Command command) {
318 logger.debug("Received command {} from channel {}", command, channelUID.getId());
320 if (command instanceof RefreshType) {
321 updateChannelsFromAlarmState(channelUID.getId(), currentState);
323 switch (channelUID.getId()) {
326 PowermaxArmMode armMode = PowermaxArmMode.fromShortName(command.toString());
328 } catch (IllegalArgumentException e) {
329 logger.debug("Powermax alarm binding: invalid command {}", command);
333 if (command instanceof OnOffType) {
335 command.equals(OnOffType.ON) ? PowermaxArmMode.ARMED_AWAY : PowermaxArmMode.DISARMED);
337 logger.debug("Command of type {} while OnOffType is expected. Command is ignored.",
338 command.getClass().getSimpleName());
344 case UPDATE_EVENT_LOGS:
351 logger.debug("No available command for channel {}. Command is ignored.", channelUID.getId());
357 private void armCommand(PowermaxArmMode armMode) {
358 if (!isConnected()) {
359 logger.debug("Powermax alarm binding not connected. Arm command is ignored.");
361 commManager.requestArmMode(armMode,
362 Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
367 private void pgmCommand(Command command) {
368 if (!isConnected()) {
369 logger.debug("Powermax alarm binding not connected. PGM command is ignored.");
371 commManager.sendPGMX10(command, null);
375 public void x10Command(Byte deviceNr, Command command) {
376 if (!isConnected()) {
377 logger.debug("Powermax alarm binding not connected. X10 command is ignored.");
379 commManager.sendPGMX10(command, deviceNr);
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");
391 commManager.sendZoneBypass(bypassed, zoneNr, getPanelSettings().getFirstPinCode());
395 private void downloadEventLog() {
396 if (!isConnected()) {
397 logger.debug("Powermax alarm binding not connected. Event logs command is ignored.");
399 commManager.requestEventLog(
400 Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
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");
413 commManager.startDownload();
414 if (currentState.lastKeepAlive.getValue() != null) {
415 currentState.lastKeepAlive.setValue(System.currentTimeMillis());
420 public String getInfoSetup() {
421 return (getPanelSettings() == null) ? "" : getPanelSettings().getInfo();
425 public void onNewStateEvent(EventObject event) {
426 PowermaxStateEvent stateEvent = (PowermaxStateEvent) event;
427 PowermaxState updateState = stateEvent.getState();
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());
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();
446 boolean doProcessSettings = (updateState.powerlinkMode.getValue() != null);
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);
455 updateState.keepOnlyDifferencesWith(currentState);
456 updateChannelsFromAlarmState(updateState);
457 currentState.merge(updateState);
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);
470 if (doProcessSettings) {
471 // There is a change of mode (standard or Powerlink)
472 processPanelSettings();
473 commManager.exitDownload();
477 private void processPanelSettings() {
478 if (commManager.processPanelSettings(Boolean.TRUE.equals(currentState.powerlinkMode.getValue()))) {
479 for (PowermaxPanelSettingsListener listener : listeners) {
480 listener.onPanelSettingsUpdated(getPanelSettings());
482 remainingDownloadAttempts = 0;
484 logger.info("Powermax alarm binding: setup download failed!");
485 for (PowermaxPanelSettingsListener listener : listeners) {
486 listener.onPanelSettingsUpdated(null);
488 remainingDownloadAttempts--;
490 updatePropertiesFromPanelSettings();
491 if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
492 logger.debug("Powermax alarm binding: running in Powerlink mode");
493 commManager.sendRestoreMessage();
495 logger.debug("Powermax alarm binding: running in Standard mode");
496 commManager.getInfosWhenInStandardMode();
501 * Update channels to match a new alarm system state
503 * @param state: the alarm system state
505 private void updateChannelsFromAlarmState(PowermaxState state) {
506 updateChannelsFromAlarmState(null, state);
510 * Update channels to match a new alarm system state
512 * @param channel: filter on a particular channel; if null, consider all channels
513 * @param state: the alarm system state
515 private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) {
520 for (Value<?> value : state.getValues()) {
521 String vChannel = value.getChannel();
523 if (((channel == null) || channel.equals(vChannel)) && (vChannel != null) && isLinked(vChannel)
524 && (value.getValue() != null)) {
525 updateState(vChannel, value.getState());
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)));
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)) {
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.
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);
553 } else if (thing.getThingTypeUID().equals(THING_TYPE_X10)) {
554 if ((channel == null) || channel.equals(X10_STATUS)) {
555 handler.updateChannelFromAlarmState(X10_STATUS, state);
564 * Update properties to match the alarm panel settings
566 private void updatePropertiesFromPanelSettings() {
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);
574 value = panelSettings.getPanelSerial();
575 if (value != null && !value.isEmpty()) {
576 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
578 value = panelSettings.getPanelEprom();
579 if (value != null && !value.isEmpty()) {
580 properties.put(Thing.PROPERTY_HARDWARE_VERSION, value);
582 value = panelSettings.getPanelSoftware();
583 if (value != null && !value.isEmpty()) {
584 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
586 updateProperties(properties);
589 public boolean registerPanelSettingsListener(PowermaxPanelSettingsListener listener) {
590 boolean inList = true;
591 if (!listeners.contains(listener)) {
592 inList = listeners.add(listener);
597 public boolean unregisterPanelSettingsListener(PowermaxPanelSettingsListener listener) {
598 return listeners.remove(listener);
601 private boolean getBooleanSetting(Boolean value, boolean defaultValue) {
602 return value != null ? value.booleanValue() : defaultValue;
605 private long getMotionOffDelaySetting(Integer value, long defaultValue) {
606 return value != null ? value.intValue() * ONE_MINUTE : defaultValue;
609 private PowermaxPanelType getPanelTypeSetting(String value, PowermaxPanelType defaultValue) {
610 PowermaxPanelType result;
613 result = PowermaxPanelType.fromLabel(value);
614 } catch (IllegalArgumentException e) {
615 result = defaultValue;
616 logger.debug("Powermax alarm binding: panel type not configured correctly");
619 result = defaultValue;