2 * Copyright (c) 2010-2024 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.dscalarm.internal.handler;
15 import static org.openhab.binding.dscalarm.internal.DSCAlarmBindingConstants.*;
17 import java.math.BigDecimal;
18 import java.text.SimpleDateFormat;
19 import java.util.Date;
20 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.openhab.binding.dscalarm.internal.DSCAlarmCode;
25 import org.openhab.binding.dscalarm.internal.DSCAlarmEvent;
26 import org.openhab.binding.dscalarm.internal.DSCAlarmMessage;
27 import org.openhab.binding.dscalarm.internal.DSCAlarmMessage.DSCAlarmMessageInfoType;
28 import org.openhab.binding.dscalarm.internal.DSCAlarmMessage.DSCAlarmMessageType;
29 import org.openhab.binding.dscalarm.internal.config.DSCAlarmPartitionConfiguration;
30 import org.openhab.binding.dscalarm.internal.config.DSCAlarmZoneConfiguration;
31 import org.openhab.binding.dscalarm.internal.discovery.DSCAlarmDiscoveryService;
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.library.types.OnOffType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.binding.BaseBridgeHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
46 * Abstract class for a DSC Alarm Bridge Handler.
48 * @author Russell Stephens - Initial Contribution
50 public abstract class DSCAlarmBaseBridgeHandler extends BaseBridgeHandler {
52 private final Logger logger = LoggerFactory.getLogger(DSCAlarmBaseBridgeHandler.class);
54 /** The DSC Alarm bridge type. */
55 private DSCAlarmBridgeType dscAlarmBridgeType = null;
57 /** The DSC Alarm bridge type. */
58 private DSCAlarmProtocol dscAlarmProtocol = null;
60 /** The DSC Alarm Discovery Service. */
61 private DSCAlarmDiscoveryService dscAlarmDiscoveryService = null;
63 /** The Panel Thing handler for the bridge. */
64 private DSCAlarmBaseThingHandler panelThingHandler = null;
66 /** Connection status for the bridge. */
67 private boolean connected = false;
69 /** Determines if things have changed. */
70 private boolean thingsHaveChanged = false;
72 /** Determines if all things have been initialized. */
73 private boolean allThingsInitialized = false;
76 private int thingCount = 0;
78 /** Password for bridge connection authentication. */
79 private String password = null;
81 /** User Code for some DSC Alarm commands. */
82 private String userCode = null;
85 protected int pollPeriod = 0;
86 private long pollElapsedTime = 0;
87 private long pollStartTime = 0;
88 private long refreshInterval = 5000;
90 private ScheduledFuture<?> pollingTask;
96 * @param dscAlarmBridgeType
98 DSCAlarmBaseBridgeHandler(Bridge bridge, DSCAlarmBridgeType dscAlarmBridgeType, DSCAlarmProtocol dscAlarmProtocol) {
100 this.dscAlarmBridgeType = dscAlarmBridgeType;
101 this.dscAlarmProtocol = dscAlarmProtocol;
105 * Returns the bridge type.
107 public DSCAlarmBridgeType getBridgeType() {
108 return dscAlarmBridgeType;
114 * @param dscAlarmProtocol
116 public void setProtocol(DSCAlarmProtocol dscAlarmProtocol) {
117 this.dscAlarmProtocol = dscAlarmProtocol;
121 * Returns the protocol.
123 public DSCAlarmProtocol getProtocol() {
124 return dscAlarmProtocol;
128 * Sets the bridge type.
130 * @param dscAlarmBridgeType
132 public void setBridgeType(DSCAlarmBridgeType dscAlarmBridgeType) {
133 this.dscAlarmBridgeType = dscAlarmBridgeType;
137 public void initialize() {
138 if (this.pollPeriod > 15) {
139 this.pollPeriod = 15;
140 } else if (this.pollPeriod < 1) {
143 updateStatus(ThingStatus.OFFLINE);
148 public void dispose() {
155 * Register the Discovery Service.
157 * @param discoveryService
159 public void registerDiscoveryService(DSCAlarmDiscoveryService discoveryService) {
160 if (discoveryService == null) {
161 throw new IllegalArgumentException("registerDiscoveryService(): Illegal Argument. Not allowed to be Null!");
163 this.dscAlarmDiscoveryService = discoveryService;
164 logger.trace("registerDiscoveryService(): Discovery Service Registered!");
169 * Unregister the Discovery Service.
171 public void unregisterDiscoveryService() {
172 dscAlarmDiscoveryService = null;
173 logger.trace("unregisterDiscoveryService(): Discovery Service Unregistered!");
177 * Connect The Bridge.
179 private void connect() {
183 if (dscAlarmBridgeType != DSCAlarmBridgeType.Envisalink) {
190 * Runs when connected.
192 public void onConnected() {
193 logger.debug("onConnected(): Bridge Connected!");
195 setBridgeStatus(true);
197 thingsHaveChanged = true;
201 * Disconnect The Bridge.
203 private void disconnect() {
206 if (!isConnected()) {
207 setBridgeStatus(false);
214 public boolean isConnected() {
215 return this.connected;
221 public void setConnected(boolean connected) {
222 this.connected = connected;
230 public void setBridgeStatus(boolean isOnline) {
231 logger.debug("setBridgeStatus(): Setting Bridge to {}", isOnline ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
233 updateStatus(isOnline ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
235 ChannelUID channelUID = new ChannelUID(getThing().getUID(), BRIDGE_RESET);
236 updateState(channelUID, OnOffType.from(isOnline));
240 * Method for opening a connection to DSC Alarm.
242 abstract void openConnection();
245 * Method for closing a connection to DSC Alarm.
247 abstract void closeConnection();
250 * Method for writing to an open DSC Alarm connection.
255 public abstract void write(String writeString, boolean doNotLog);
258 * Method for reading from an open DSC Alarm connection.
260 public abstract String read();
263 * Get Bridge Password.
265 public String getPassword() {
266 return this.password;
270 * Set Bridge Password.
274 public void setPassword(String password) {
275 this.password = password;
279 * Get Panel User Code.
281 public String getUserCode() {
282 return this.userCode;
286 * Set Panel User Code.
290 public void setUserCode(String userCode) {
291 this.userCode = userCode;
295 * Method to start the polling task.
297 private void startPolling() {
298 logger.debug("Starting DSC Alarm Polling Task.");
299 if (pollingTask == null || pollingTask.isCancelled()) {
300 pollingTask = scheduler.scheduleWithFixedDelay(this::polling, 0, refreshInterval, TimeUnit.MILLISECONDS);
305 * Method to stop the polling task.
307 private void stopPolling() {
308 logger.debug("Stopping DSC Alarm Polling Task.");
309 if (pollingTask != null && !pollingTask.isCancelled()) {
310 pollingTask.cancel(true);
316 * Method for polling the DSC Alarm System.
318 public synchronized void polling() {
319 logger.debug("DSC Alarm Polling Task - '{}' is {}", getThing().getUID(), getThing().getStatus());
322 if (pollStartTime == 0) {
323 pollStartTime = System.currentTimeMillis();
326 pollElapsedTime = ((System.currentTimeMillis() - pollStartTime) / 1000) / 60;
328 // Send Poll command to the DSC Alarm if idle for 'pollPeriod'
330 if (pollElapsedTime >= pollPeriod) {
331 sendCommand(DSCAlarmCode.Poll);
337 if (thingsHaveChanged && allThingsInitialized) {
338 this.setBridgeStatus(isConnected());
339 thingsHaveChanged = false;
340 // Get a status report from DSC Alarm.
341 sendCommand(DSCAlarmCode.StatusReport);
344 logger.error("Not Connected to the DSC Alarm!");
350 * Check if things have changed.
352 public void checkThings() {
353 logger.debug("Checking Things!");
355 allThingsInitialized = true;
357 List<Thing> things = getThing().getThings();
359 if (things.size() != thingCount) {
360 thingsHaveChanged = true;
361 thingCount = things.size();
364 for (Thing thing : things) {
366 DSCAlarmBaseThingHandler handler = (DSCAlarmBaseThingHandler) thing.getHandler();
368 if (handler != null) {
369 logger.debug("***Checking '{}' - Status: {}, Initialized: {}", thing.getUID(), thing.getStatus(),
370 handler.isThingHandlerInitialized());
372 if (!handler.isThingHandlerInitialized() || thing.getStatus() != ThingStatus.ONLINE) {
373 allThingsInitialized = false;
376 if (handler.getDSCAlarmThingType().equals(DSCAlarmThingType.PANEL)) {
377 if (panelThingHandler == null) {
378 panelThingHandler = handler;
383 logger.error("checkThings(): Thing handler not found!");
391 * @param dscAlarmThingType
396 public Thing findThing(DSCAlarmThingType dscAlarmThingType, int partitionId, int zoneId) {
397 List<Thing> things = getThing().getThings();
401 for (Thing t : things) {
403 Configuration config = t.getConfiguration();
404 DSCAlarmBaseThingHandler handler = (DSCAlarmBaseThingHandler) t.getHandler();
406 if (handler != null) {
407 DSCAlarmThingType handlerDSCAlarmThingType = handler.getDSCAlarmThingType();
409 if (handlerDSCAlarmThingType != null) {
410 if (handlerDSCAlarmThingType.equals(dscAlarmThingType)) {
411 switch (handlerDSCAlarmThingType) {
415 logger.debug("findThing(): Thing Found - {}, {}, {}", t, handler,
416 handlerDSCAlarmThingType);
419 BigDecimal partitionNumber = (BigDecimal) config
420 .get(DSCAlarmPartitionConfiguration.PARTITION_NUMBER);
421 if (partitionId == partitionNumber.intValue()) {
423 logger.debug("findThing(): Thing Found - {}, {}, {}", t, handler,
424 handlerDSCAlarmThingType);
429 BigDecimal zoneNumber = (BigDecimal) config
430 .get(DSCAlarmZoneConfiguration.ZONE_NUMBER);
431 if (zoneId == zoneNumber.intValue()) {
433 logger.debug("findThing(): Thing Found - {}, {}, {}", t, handler,
434 handlerDSCAlarmThingType);
444 } catch (Exception e) {
445 logger.debug("findThing(): Error Seaching Thing - {} ", e.getMessage(), e);
453 * Handles an incoming message from the DSC Alarm System.
455 * @param incomingMessage
457 public synchronized void handleIncomingMessage(String incomingMessage) {
458 if (incomingMessage != null && !incomingMessage.isEmpty()) {
459 DSCAlarmMessage dscAlarmMessage = new DSCAlarmMessage(incomingMessage);
460 DSCAlarmMessageType dscAlarmMessageType = dscAlarmMessage.getDSCAlarmMessageType();
462 logger.debug("handleIncomingMessage(): Message received: {} - {}", incomingMessage,
463 dscAlarmMessage.toString());
465 DSCAlarmEvent event = new DSCAlarmEvent(this);
466 event.dscAlarmEventMessage(dscAlarmMessage);
467 DSCAlarmThingType dscAlarmThingType = null;
471 DSCAlarmCode dscAlarmCode = DSCAlarmCode
472 .getDSCAlarmCodeValue(dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.CODE));
474 if (panelThingHandler != null) {
475 panelThingHandler.setPanelMessage(dscAlarmMessage);
478 if (dscAlarmCode == DSCAlarmCode.LoginResponse) {
479 String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
480 if ("3".equals(dscAlarmMessageData)) {
481 sendCommand(DSCAlarmCode.NetworkLogin);
483 } else if ("1".equals(dscAlarmMessageData)) {
487 } else if (dscAlarmCode == DSCAlarmCode.CommandAcknowledge) {
488 String dscAlarmMessageData = dscAlarmMessage.getMessageInfo(DSCAlarmMessageInfoType.DATA);
489 if ("000".equals(dscAlarmMessageData)) {
490 setBridgeStatus(true);
494 switch (dscAlarmMessageType) {
496 dscAlarmThingType = DSCAlarmThingType.PANEL;
498 case PARTITION_EVENT:
499 dscAlarmThingType = DSCAlarmThingType.PARTITION;
500 partitionId = Integer
501 .parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.PARTITION));
504 dscAlarmThingType = DSCAlarmThingType.ZONE;
505 zoneId = Integer.parseInt(event.getDSCAlarmMessage().getMessageInfo(DSCAlarmMessageInfoType.ZONE));
508 dscAlarmThingType = DSCAlarmThingType.KEYPAD;
514 if (dscAlarmThingType != null) {
515 Thing thing = findThing(dscAlarmThingType, partitionId, zoneId);
517 logger.debug("handleIncomingMessage(): Thing Search - '{}'", thing);
520 DSCAlarmBaseThingHandler thingHandler = (DSCAlarmBaseThingHandler) thing.getHandler();
522 if (thingHandler != null) {
523 if (thingHandler.isThingHandlerInitialized() && thing.getStatus() == ThingStatus.ONLINE) {
524 thingHandler.dscAlarmEventReceived(event, thing);
527 logger.debug("handleIncomingMessage(): Thing '{}' Not Refreshed!", thing.getUID());
531 logger.debug("handleIncomingMessage(): Thing Not Found! Send to Discovery Service!");
533 if (dscAlarmDiscoveryService != null) {
534 dscAlarmDiscoveryService.addThing(getThing(), dscAlarmThingType, event);
539 logger.debug("handleIncomingMessage(): No Message Received!");
544 public void handleCommand(ChannelUID channelUID, Command command) {
545 logger.debug("handleCommand(): Command Received - {} {}.", channelUID, command);
547 if (command instanceof RefreshType) {
552 switch (channelUID.getId()) {
554 if (command == OnOffType.OFF) {
559 if (!command.toString().isEmpty()) {
560 String[] tokens = command.toString().split(",");
562 String cmd = tokens[0];
564 if (tokens.length > 1) {
568 sendDSCAlarmCommand(cmd, data);
570 updateState(channelUID, new StringType(""));
580 * Method to send a sequence of key presses one at a time using the '070' command.
584 private boolean sendKeySequence(String keySequence) {
585 logger.debug("sendKeySequence(): Sending key sequence '{}'.", keySequence);
587 boolean sent = false;
589 for (char key : keySequence.toCharArray()) {
590 sent = sendCommand(DSCAlarmCode.KeyStroke, String.valueOf(key));
601 * Sends a DSC Alarm command
606 public boolean sendDSCAlarmCommand(String command, String data) {
607 logger.debug("sendDSCAlarmCommand(): Attempting to send DSC Alarm Command: command - {} - data: {}", command,
610 DSCAlarmCode dscAlarmCode = DSCAlarmCode.getDSCAlarmCodeValue(command);
612 if (dscAlarmProtocol.equals(DSCAlarmProtocol.IT100_API) && dscAlarmCode.equals(DSCAlarmCode.KeySequence)) {
613 return sendKeySequence(data);
615 return sendCommand(dscAlarmCode, data);
620 * Send an API command to the DSC Alarm system.
622 * @param dscAlarmCode
623 * @param dscAlarmData
626 public boolean sendCommand(DSCAlarmCode dscAlarmCode, String... dscAlarmData) {
627 boolean successful = false;
628 boolean validCommand = false;
630 String command = dscAlarmCode.getCode();
632 boolean confidentialData = false;
634 switch (dscAlarmCode) {
636 case StatusReport: /* 001 */
639 case LabelsRequest: /* 002 */
640 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.IT100_API)) {
645 case NetworkLogin: /* 005 */
646 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.ENVISALINK_TPI)) {
650 if (password == null) {
651 logger.error("sendCommand(): No password!");
655 confidentialData = true;
658 case DumpZoneTimers: /* 008 */
659 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.ENVISALINK_TPI)) {
664 case SetTimeDate: /* 010 */
665 Date date = new Date();
666 SimpleDateFormat dateTime = new SimpleDateFormat("HHmmMMddYY");
667 data = dateTime.format(date);
670 case CommandOutputControl: /* 020 */
671 if (dscAlarmData[0] == null || !dscAlarmData[0].matches("[1-8]")) {
673 "sendCommand(): Partition number must be a single character string from 1 to 8, it was: {}",
678 if (dscAlarmData[1] == null || !dscAlarmData[1].matches("[1-4]")) {
680 "sendCommand(): Output number must be a single character string from 1 to 4, it was: {}",
685 data = dscAlarmData[0];
688 case KeepAlive: /* 074 */
689 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.ENVISALINK_TPI)) {
692 case PartitionArmControlAway: /* 030 */
693 case PartitionArmControlStay: /* 031 */
694 case PartitionArmControlZeroEntryDelay: /* 032 */
695 if (dscAlarmData[0] == null || !dscAlarmData[0].matches("[1-8]")) {
697 "sendCommand(): Partition number must be a single character string from 1 to 8, it was: {}",
701 data = dscAlarmData[0];
704 case PartitionArmControlWithUserCode: /* 033 */
705 case PartitionDisarmControl: /* 040 */
706 if (dscAlarmData[0] == null || !dscAlarmData[0].matches("[1-8]")) {
708 "sendCommand(): Partition number must be a single character string from 1 to 8, it was: {}",
713 if (userCode == null || userCode.length() < 4 || userCode.length() > 6) {
714 logger.error("sendCommand(): User Code is invalid, must be between 4 and 6 chars");
718 if (dscAlarmProtocol.equals(DSCAlarmProtocol.IT100_API)) {
719 data = dscAlarmData[0] + String.format("%-6s", userCode).replace(' ', '0');
721 data = dscAlarmData[0] + userCode;
724 confidentialData = true;
727 case VirtualKeypadControl: /* 058 */
728 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.IT100_API)) {
731 case TimeStampControl: /* 055 */
732 case TimeDateBroadcastControl: /* 056 */
733 case TemperatureBroadcastControl: /* 057 */
734 if (dscAlarmData[0] == null || !dscAlarmData[0].matches("[0-1]")) {
735 logger.error("sendCommand(): Value must be a single character string of 0 or 1: {}",
739 data = dscAlarmData[0];
742 case TriggerPanicAlarm: /* 060 */
743 if (dscAlarmData[0] == null || !dscAlarmData[0].matches("[1-3]")) {
744 logger.error("sendCommand(): FAPcode must be a single character string from 1 to 3, it was: {}",
748 data = dscAlarmData[0];
751 case KeyStroke: /* 070 */
752 if (dscAlarmProtocol.equals(DSCAlarmProtocol.ENVISALINK_TPI)) {
753 if (dscAlarmData[0] == null || dscAlarmData[0].length() != 1
754 || !dscAlarmData[0].matches("[0-9]|A|#|\\*")) {
756 "sendCommand(): \'keystroke\' must be a single character string from 0 to 9, *, #, or A, it was: {}",
760 } else if (dscAlarmProtocol.equals(DSCAlarmProtocol.IT100_API)) {
761 if (dscAlarmData[0] == null || dscAlarmData[0].length() != 1
762 || !dscAlarmData[0].matches("[0-9]|\\*|#|F|A|P|[a-e]|<|>|=|\\^|L")) {
764 "sendCommand(): \'keystroke\' must be a single character string from 0 to 9, *, #, F, A, P, a to e, <, >, =, or ^, it was: {}",
767 } else if ("L".equals(dscAlarmData[0])) { /* Long Key Press */
773 } catch (InterruptedException e) {
774 logger.error("sendCommand(): \'keystroke\': Error with Long Key Press!");
782 data = dscAlarmData[0];
785 case KeySequence: /* 071 */
786 if (!dscAlarmProtocol.equals(DSCAlarmProtocol.ENVISALINK_TPI)) {
790 if (dscAlarmData[0] == null || dscAlarmData[0].length() > 6
791 || !dscAlarmData[0].matches("(\\d|#|\\*)+")) {
793 "sendCommand(): \'keysequence\' must be a string of up to 6 characters consiting of 0 to 9, *, or #, it was: {}",
797 data = dscAlarmData[0];
800 case CodeSend: /* 200 */
801 if (userCode == null || userCode.length() < 4 || userCode.length() > 6) {
802 logger.error("sendCommand(): Access Code is invalid, must be between 4 and 6 chars");
807 confidentialData = true;
812 validCommand = false;
818 String cmd = dscAlarmCommand(command, data);
819 write(cmd, confidentialData);
821 logger.debug("sendCommand(): '{}' Command Sent - {}", dscAlarmCode, confidentialData ? "***" : cmd);
823 logger.error("sendCommand(): Command '{}' Not Sent - Invalid!", dscAlarmCode);
829 private String dscAlarmCommand(String command, String data) {
832 String cmd = command + data;
834 for (int i = 0; i < cmd.length(); i++) {
835 char c = cmd.charAt(i);
841 String strChecksum = Integer.toHexString(sum >> 4) + Integer.toHexString(sum & 0xF);
843 return cmd + strChecksum.toUpperCase() + "\r\n";