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.message.PowermaxSendType;
31 import org.openhab.binding.powermax.internal.state.PowermaxArmMode;
32 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettings;
33 import org.openhab.binding.powermax.internal.state.PowermaxPanelSettingsListener;
34 import org.openhab.binding.powermax.internal.state.PowermaxPanelType;
35 import org.openhab.binding.powermax.internal.state.PowermaxState;
36 import org.openhab.binding.powermax.internal.state.PowermaxStateContainer.Value;
37 import org.openhab.binding.powermax.internal.state.PowermaxStateEvent;
38 import org.openhab.binding.powermax.internal.state.PowermaxStateEventListener;
39 import org.openhab.core.i18n.TimeZoneProvider;
40 import org.openhab.core.io.transport.serial.SerialPortManager;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.UnDefType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * The {@link PowermaxBridgeHandler} is responsible for handling commands, which are
58 * sent to one of the channels.
60 * @author Laurent Garnier - Initial contribution
62 public class PowermaxBridgeHandler extends BaseBridgeHandler implements PowermaxStateEventListener {
64 private final Logger logger = LoggerFactory.getLogger(PowermaxBridgeHandler.class);
65 private final SerialPortManager serialPortManager;
66 private final TimeZoneProvider timeZoneProvider;
68 private static final long ONE_MINUTE = TimeUnit.MINUTES.toMillis(1);
69 private static final long FIVE_MINUTES = TimeUnit.MINUTES.toMillis(5);
71 /** Default delay in milliseconds to reset a motion detection */
72 private static final long DEFAULT_MOTION_OFF_DELAY = TimeUnit.MINUTES.toMillis(3);
74 private static final int NB_EVENT_LOG = 10;
76 private static final PowermaxPanelType DEFAULT_PANEL_TYPE = PowermaxPanelType.POWERMAX_PRO;
78 private static final int JOB_REPEAT = 20;
80 private static final int MAX_DOWNLOAD_ATTEMPTS = 3;
82 private ScheduledFuture<?> globalJob;
84 private List<PowermaxPanelSettingsListener> listeners = new CopyOnWriteArrayList<>();
86 /** The delay in milliseconds to reset a motion detection */
87 private long motionOffDelay;
89 /** The PIN code to use for arming/disarming the Powermax alarm system from openHAB */
90 private String pinCode;
92 /** Force the standard mode rather than trying using the Powerlink mode */
93 private boolean forceStandardMode;
95 /** The object to store the current state of the Powermax alarm system */
96 private PowermaxState currentState;
98 /** The object in charge of the communication with the Powermax alarm system */
99 private PowermaxCommManager commManager;
101 private int remainingDownloadAttempts;
103 public PowermaxBridgeHandler(Bridge thing, SerialPortManager serialPortManager, TimeZoneProvider timeZoneProvider) {
105 this.serialPortManager = serialPortManager;
106 this.timeZoneProvider = timeZoneProvider;
110 public Collection<Class<? extends ThingHandlerService>> getServices() {
111 return Collections.singleton(PowermaxDiscoveryService.class);
114 public PowermaxState getCurrentState() {
118 public PowermaxPanelSettings getPanelSettings() {
119 return (commManager == null) ? null : commManager.getPanelSettings();
123 public void initialize() {
124 logger.debug("initializing handler for thing {}", getThing().getUID());
128 String threadName = "OH-binding-" + getThing().getUID().getAsString();
130 String errorMsg = null;
131 if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_SERIAL)) {
132 errorMsg = initializeBridgeSerial(getConfigAs(PowermaxSerialConfiguration.class), threadName);
133 } else if (getThing().getThingTypeUID().equals(BRIDGE_TYPE_IP)) {
134 errorMsg = initializeBridgeIp(getConfigAs(PowermaxIpConfiguration.class), threadName);
136 errorMsg = "Unexpected thing type " + getThing().getThingTypeUID();
139 if (errorMsg == null) {
140 if (globalJob == null || globalJob.isCancelled()) {
141 // Delay the startup in case the handler is restarted immediately
142 globalJob = scheduler.scheduleWithFixedDelay(() -> {
144 logger.trace("Powermax job...");
145 updateMotionSensorState();
146 updateRingingState();
149 commManager.retryDownloadSetup(remainingDownloadAttempts);
153 } catch (Exception e) {
154 logger.warn("Exception in scheduled job: {}", e.getMessage(), e);
156 }, 10, JOB_REPEAT, TimeUnit.SECONDS);
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMsg);
163 private String initializeBridgeSerial(PowermaxSerialConfiguration config, String threadName) {
164 String errorMsg = null;
165 if (config.serialPort != null && !config.serialPort.trim().isEmpty()
166 && !config.serialPort.trim().startsWith("rfc2217")) {
167 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
168 boolean allowArming = getBooleanSetting(config.allowArming, false);
169 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
170 pinCode = config.pinCode;
171 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
172 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
173 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
175 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
176 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
177 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
178 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
179 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
180 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
181 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
183 commManager = new PowermaxCommManager(config.serialPort, panelType, forceStandardMode, autoSyncTime,
184 serialPortManager, threadName, timeZoneProvider);
186 if (config.serialPort != null && config.serialPort.trim().startsWith("rfc2217")) {
187 errorMsg = "Please use the IP Connection thing type for a serial over IP connection.";
189 errorMsg = "serialPort setting must be defined in thing configuration";
195 private String initializeBridgeIp(PowermaxIpConfiguration config, String threadName) {
196 String errorMsg = null;
197 if (config.ip != null && !config.ip.trim().isEmpty() && config.tcpPort != null) {
198 motionOffDelay = getMotionOffDelaySetting(config.motionOffDelay, DEFAULT_MOTION_OFF_DELAY);
199 boolean allowArming = getBooleanSetting(config.allowArming, false);
200 boolean allowDisarming = getBooleanSetting(config.allowDisarming, false);
201 pinCode = config.pinCode;
202 forceStandardMode = getBooleanSetting(config.forceStandardMode, false);
203 PowermaxPanelType panelType = getPanelTypeSetting(config.panelType, DEFAULT_PANEL_TYPE);
204 boolean autoSyncTime = getBooleanSetting(config.autoSyncTime, false);
206 PowermaxArmMode.DISARMED.setAllowedCommand(allowDisarming);
207 PowermaxArmMode.ARMED_HOME.setAllowedCommand(allowArming);
208 PowermaxArmMode.ARMED_AWAY.setAllowedCommand(allowArming);
209 PowermaxArmMode.ARMED_HOME_INSTANT.setAllowedCommand(allowArming);
210 PowermaxArmMode.ARMED_AWAY_INSTANT.setAllowedCommand(allowArming);
211 PowermaxArmMode.ARMED_NIGHT.setAllowedCommand(allowArming);
212 PowermaxArmMode.ARMED_NIGHT_INSTANT.setAllowedCommand(allowArming);
214 commManager = new PowermaxCommManager(config.ip, config.tcpPort, panelType, forceStandardMode, autoSyncTime,
215 threadName, timeZoneProvider);
217 errorMsg = "ip and port settings must be defined in thing configuration";
223 public void dispose() {
224 logger.debug("Handler disposed for thing {}", getThing().getUID());
225 if (globalJob != null && !globalJob.isCancelled()) {
226 globalJob.cancel(true);
235 * Set the state of items linked to motion sensors to OFF when the last trip is older
236 * than the value defined by the variable motionOffDelay
238 private void updateMotionSensorState() {
239 long now = System.currentTimeMillis();
240 if (currentState != null) {
241 boolean update = false;
242 PowermaxState updateState = commManager.createNewState();
243 PowermaxPanelSettings panelSettings = getPanelSettings();
244 for (int i = 1; i <= panelSettings.getNbZones(); i++) {
245 if (panelSettings.getZoneSettings(i) != null && panelSettings.getZoneSettings(i).isMotionSensor()
246 && currentState.getZone(i).isLastTripBeforeTime(now - motionOffDelay)) {
248 updateState.getZone(i).tripped.setValue(false);
252 updateChannelsFromAlarmState(TRIPPED, updateState);
253 currentState.merge(updateState);
259 * Turn off the Ringing flag when the bell time expires
261 private void updateRingingState() {
262 if (currentState != null && Boolean.TRUE.equals(currentState.ringing.getValue())) {
263 long now = System.currentTimeMillis();
264 long bellTime = getPanelSettings().getBellTime() * ONE_MINUTE;
266 if ((currentState.ringingSince.getValue() + bellTime) < now) {
267 PowermaxState updateState = commManager.createNewState();
268 updateState.ringing.setValue(false);
269 updateChannelsFromAlarmState(RINGING, updateState);
270 currentState.merge(updateState);
276 * Check that we're actively communicating with the panel
278 private void checkKeepAlive() {
279 long now = System.currentTimeMillis();
280 if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())
281 && (currentState.lastKeepAlive.getValue() != null)
282 && ((now - currentState.lastKeepAlive.getValue()) > ONE_MINUTE)) {
283 // In Powerlink mode: let Powermax know we are alive
284 commManager.sendRestoreMessage();
285 currentState.lastKeepAlive.setValue(now);
286 } else if (!Boolean.TRUE.equals(currentState.downloadMode.getValue())
287 && (currentState.lastMessageTime.getValue() != null)
288 && ((now - currentState.lastMessageTime.getValue()) > FIVE_MINUTES)) {
289 // In Standard mode: ping the panel every so often to detect disconnects
290 commManager.sendMessage(PowermaxSendType.STATUS);
294 private void tryReconnect() {
295 logger.info("Trying to connect or reconnect...");
297 currentState = commManager.createNewState();
300 logger.debug("openConnection(): connected");
301 updateStatus(ThingStatus.ONLINE);
302 updateChannelsFromAlarmState(currentState);
303 if (forceStandardMode) {
304 currentState.powerlinkMode.setValue(false);
305 updateChannelsFromAlarmState(MODE, currentState);
306 processPanelSettings();
308 commManager.startDownload();
310 } catch (Exception e) {
311 logger.debug("openConnection(): {}", e.getMessage(), e);
312 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
313 setAllChannelsOffline();
318 * Open a TCP or Serial connection to the Powermax Alarm Panel
320 * @return true if the connection has been opened
322 private synchronized void openConnection() throws Exception {
323 if (commManager != null) {
324 commManager.addEventListener(this);
327 remainingDownloadAttempts = MAX_DOWNLOAD_ATTEMPTS;
331 * Close TCP or Serial connection to the Powermax Alarm Panel and remove the Event Listener
333 private synchronized void closeConnection() {
334 if (commManager != null) {
336 commManager.removeEventListener(this);
338 logger.debug("closeConnection(): disconnected");
341 private boolean isConnected() {
342 return commManager == null ? false : commManager.isConnected();
346 public void handleCommand(ChannelUID channelUID, Command command) {
347 logger.debug("Received command {} from channel {}", command, channelUID.getId());
349 if (command instanceof RefreshType) {
350 updateChannelsFromAlarmState(channelUID.getId(), currentState);
352 switch (channelUID.getId()) {
355 PowermaxArmMode armMode = PowermaxArmMode.fromShortName(command.toString());
357 } catch (IllegalArgumentException e) {
358 logger.debug("Powermax alarm binding: invalid command {}", command);
362 if (command instanceof OnOffType) {
364 command.equals(OnOffType.ON) ? PowermaxArmMode.ARMED_AWAY : PowermaxArmMode.DISARMED);
366 logger.debug("Command of type {} while OnOffType is expected. Command is ignored.",
367 command.getClass().getSimpleName());
373 case UPDATE_EVENT_LOGS:
380 logger.debug("No available command for channel {}. Command is ignored.", channelUID.getId());
386 private void armCommand(PowermaxArmMode armMode) {
387 if (!isConnected()) {
388 logger.debug("Powermax alarm binding not connected. Arm command is ignored.");
390 commManager.requestArmMode(armMode,
391 Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
396 private void pgmCommand(Command command) {
397 if (!isConnected()) {
398 logger.debug("Powermax alarm binding not connected. PGM command is ignored.");
400 commManager.sendPGMX10(command, null);
404 public void x10Command(Byte deviceNr, Command command) {
405 if (!isConnected()) {
406 logger.debug("Powermax alarm binding not connected. X10 command is ignored.");
408 commManager.sendPGMX10(command, deviceNr);
412 public void zoneBypassed(byte zoneNr, boolean bypassed) {
413 if (!isConnected()) {
414 logger.debug("Powermax alarm binding not connected. Zone bypass command is ignored.");
415 } else if (!Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
416 logger.debug("Powermax alarm binding: Bypass option only supported in Powerlink mode");
417 } else if (!getPanelSettings().isBypassEnabled()) {
418 logger.debug("Powermax alarm binding: Bypass option not enabled in panel settings");
420 commManager.sendZoneBypass(bypassed, zoneNr, getPanelSettings().getFirstPinCode());
424 private void downloadEventLog() {
425 if (!isConnected()) {
426 logger.debug("Powermax alarm binding not connected. Event logs command is ignored.");
428 commManager.requestEventLog(
429 Boolean.TRUE.equals(currentState.powerlinkMode.getValue()) ? getPanelSettings().getFirstPinCode()
434 public void downloadSetup() {
435 if (!isConnected()) {
436 logger.debug("Powermax alarm binding not connected. Download setup command is ignored.");
437 } else if (!Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
438 logger.debug("Powermax alarm binding: download setup only supported in Powerlink mode");
439 } else if (commManager.isDownloadRunning()) {
440 logger.debug("Powermax alarm binding: download setup not started as one is in progress");
442 commManager.startDownload();
443 if (currentState.lastKeepAlive.getValue() != null) {
444 currentState.lastKeepAlive.setValue(System.currentTimeMillis());
449 public String getInfoSetup() {
450 return (getPanelSettings() == null) ? "" : getPanelSettings().getInfo();
454 public void onNewStateEvent(EventObject event) {
455 PowermaxStateEvent stateEvent = (PowermaxStateEvent) event;
456 PowermaxState updateState = stateEvent.getState();
458 if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())
459 && Boolean.TRUE.equals(updateState.downloadSetupRequired.getValue())) {
460 // After Enrolling Powerlink or if a reset is required
461 logger.debug("Powermax alarm binding: Reset");
462 commManager.startDownload();
463 updateState.downloadSetupRequired.setValue(false);
464 if (currentState.lastKeepAlive.getValue() != null) {
465 currentState.lastKeepAlive.setValue(System.currentTimeMillis());
467 } else if (Boolean.FALSE.equals(currentState.powerlinkMode.getValue())
468 && updateState.lastKeepAlive.getValue() != null) {
469 // Were are in standard mode but received a keep alive message
470 // so we switch in PowerLink mode
471 logger.debug("Powermax alarm binding: Switching to Powerlink mode");
472 commManager.startDownload();
475 boolean doProcessSettings = (updateState.powerlinkMode.getValue() != null);
477 getPanelSettings().getZoneRange().forEach(i -> {
478 if (Boolean.TRUE.equals(updateState.getZone(i).armed.getValue())
479 && Boolean.TRUE.equals(currentState.getZone(i).bypassed.getValue())) {
480 updateState.getZone(i).armed.setValue(false);
484 updateState.keepOnlyDifferencesWith(currentState);
485 updateChannelsFromAlarmState(updateState);
486 currentState.merge(updateState);
488 PowermaxPanelSettings panelSettings = getPanelSettings();
489 if (!updateState.getUpdatedZoneNames().isEmpty()) {
490 for (Integer zoneIdx : updateState.getUpdatedZoneNames().keySet()) {
491 if (panelSettings.getZoneSettings(zoneIdx) != null) {
492 for (PowermaxPanelSettingsListener listener : listeners) {
493 listener.onZoneSettingsUpdated(zoneIdx, panelSettings);
499 if (doProcessSettings) {
500 // There is a change of mode (standard or Powerlink)
501 processPanelSettings();
502 commManager.exitDownload();
507 public void onCommunicationFailure(String message) {
508 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
509 setAllChannelsOffline();
512 private void processPanelSettings() {
513 if (commManager.processPanelSettings(Boolean.TRUE.equals(currentState.powerlinkMode.getValue()))) {
514 for (PowermaxPanelSettingsListener listener : listeners) {
515 listener.onPanelSettingsUpdated(getPanelSettings());
517 remainingDownloadAttempts = 0;
519 logger.info("Powermax alarm binding: setup download failed!");
520 for (PowermaxPanelSettingsListener listener : listeners) {
521 listener.onPanelSettingsUpdated(null);
523 remainingDownloadAttempts--;
525 updatePropertiesFromPanelSettings();
526 if (Boolean.TRUE.equals(currentState.powerlinkMode.getValue())) {
527 logger.info("Powermax alarm binding: running in Powerlink mode");
528 commManager.sendRestoreMessage();
530 logger.info("Powermax alarm binding: running in Standard mode");
531 commManager.getInfosWhenInStandardMode();
536 * Update channels to match a new alarm system state
538 * @param state: the alarm system state
540 private void updateChannelsFromAlarmState(PowermaxState state) {
541 updateChannelsFromAlarmState(null, state);
545 * Update channels to match a new alarm system state
547 * @param channel: filter on a particular channel; if null, consider all channels
548 * @param state: the alarm system state
550 private synchronized void updateChannelsFromAlarmState(String channel, PowermaxState state) {
551 if (state == null || !isConnected()) {
555 for (Value<?> value : state.getValues()) {
556 String vChannel = value.getChannel();
558 if (((channel == null) || channel.equals(vChannel)) && (vChannel != null) && isLinked(vChannel)
559 && (value.getValue() != null)) {
560 updateState(vChannel, value.getState());
564 for (int i = 1; i <= NB_EVENT_LOG; i++) {
565 String channel2 = String.format(EVENT_LOG, i);
566 if (((channel == null) || channel.equals(channel2)) && isLinked(channel2)
567 && (state.getEventLog(i) != null)) {
568 updateState(channel2, new StringType(state.getEventLog(i)));
572 for (Thing thing : getThing().getThings()) {
573 if (thing.getHandler() != null) {
574 PowermaxThingHandler handler = (PowermaxThingHandler) thing.getHandler();
575 if (handler != null) {
576 if (thing.getThingTypeUID().equals(THING_TYPE_ZONE)) {
578 // All of the zone state objects will have the same list of values.
579 // The use of getZone(1) here is just to get any PowermaxZoneState
580 // and use it to get the list of zone channels.
582 for (Value<?> value : state.getZone(1).getValues()) {
583 String channelId = value.getChannel();
584 if ((channelId != null) && ((channel == null) || channel.equals(channelId))) {
585 handler.updateChannelFromAlarmState(channelId, state);
588 } else if (thing.getThingTypeUID().equals(THING_TYPE_X10)) {
589 if ((channel == null) || channel.equals(X10_STATUS)) {
590 handler.updateChannelFromAlarmState(X10_STATUS, state);
599 * Update all channels to an UNDEF state to indicate that communication with the panel is offline
601 private synchronized void setAllChannelsOffline() {
602 getThing().getChannels().forEach(c -> updateState(c.getUID(), UnDefType.UNDEF));
606 * Update properties to match the alarm panel settings
608 private void updatePropertiesFromPanelSettings() {
610 Map<String, String> properties = editProperties();
611 PowermaxPanelSettings panelSettings = getPanelSettings();
612 value = (panelSettings.getPanelType() != null) ? panelSettings.getPanelType().getLabel() : null;
613 if (value != null && !value.isEmpty()) {
614 properties.put(Thing.PROPERTY_MODEL_ID, value);
616 value = panelSettings.getPanelSerial();
617 if (value != null && !value.isEmpty()) {
618 properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
620 value = panelSettings.getPanelEprom();
621 if (value != null && !value.isEmpty()) {
622 properties.put(Thing.PROPERTY_HARDWARE_VERSION, value);
624 value = panelSettings.getPanelSoftware();
625 if (value != null && !value.isEmpty()) {
626 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
628 updateProperties(properties);
631 public boolean registerPanelSettingsListener(PowermaxPanelSettingsListener listener) {
632 boolean inList = true;
633 if (!listeners.contains(listener)) {
634 inList = listeners.add(listener);
639 public boolean unregisterPanelSettingsListener(PowermaxPanelSettingsListener listener) {
640 return listeners.remove(listener);
643 private boolean getBooleanSetting(Boolean value, boolean defaultValue) {
644 return value != null ? value.booleanValue() : defaultValue;
647 private long getMotionOffDelaySetting(Integer value, long defaultValue) {
648 return value != null ? value.intValue() * ONE_MINUTE : defaultValue;
651 private PowermaxPanelType getPanelTypeSetting(String value, PowermaxPanelType defaultValue) {
652 PowermaxPanelType result;
655 result = PowermaxPanelType.fromLabel(value);
656 } catch (IllegalArgumentException e) {
657 result = defaultValue;
658 logger.debug("Powermax alarm binding: panel type not configured correctly");
661 result = defaultValue;