2 * Copyright (c) 2010-2023 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.lutron.internal.handler;
15 import static org.openhab.binding.lutron.internal.LutronBindingConstants.*;
17 import java.io.BufferedReader;
18 import java.io.BufferedWriter;
19 import java.io.FileInputStream;
20 import java.io.FileNotFoundException;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.io.InterruptedIOException;
24 import java.io.OutputStreamWriter;
25 import java.net.UnknownHostException;
26 import java.security.KeyManagementException;
27 import java.security.KeyStore;
28 import java.security.KeyStoreException;
29 import java.security.NoSuchAlgorithmException;
30 import java.security.UnrecoverableKeyException;
31 import java.security.cert.CertificateException;
32 import java.security.cert.X509Certificate;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.List;
38 import java.util.concurrent.BlockingQueue;
39 import java.util.concurrent.ConcurrentHashMap;
40 import java.util.concurrent.Future;
41 import java.util.concurrent.LinkedBlockingQueue;
42 import java.util.concurrent.ScheduledFuture;
43 import java.util.concurrent.TimeUnit;
45 import javax.net.ssl.KeyManagerFactory;
46 import javax.net.ssl.SSLContext;
47 import javax.net.ssl.SSLSocket;
48 import javax.net.ssl.SSLSocketFactory;
49 import javax.net.ssl.TrustManager;
50 import javax.net.ssl.TrustManagerFactory;
51 import javax.net.ssl.X509TrustManager;
53 import org.eclipse.jdt.annotation.NonNullByDefault;
54 import org.eclipse.jdt.annotation.Nullable;
55 import org.openhab.binding.lutron.internal.config.LeapBridgeConfig;
56 import org.openhab.binding.lutron.internal.discovery.LeapDeviceDiscoveryService;
57 import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
58 import org.openhab.binding.lutron.internal.protocol.GroupCommand;
59 import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
60 import org.openhab.binding.lutron.internal.protocol.OutputCommand;
61 import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
62 import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParser;
63 import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParserCallbacks;
64 import org.openhab.binding.lutron.internal.protocol.leap.Request;
65 import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
66 import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
67 import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
68 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
69 import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
70 import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
71 import org.openhab.core.library.types.StringType;
72 import org.openhab.core.thing.Bridge;
73 import org.openhab.core.thing.ChannelUID;
74 import org.openhab.core.thing.Thing;
75 import org.openhab.core.thing.ThingStatus;
76 import org.openhab.core.thing.ThingStatusDetail;
77 import org.openhab.core.thing.ThingStatusInfo;
78 import org.openhab.core.thing.binding.ThingHandler;
79 import org.openhab.core.thing.binding.ThingHandlerService;
80 import org.openhab.core.types.Command;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
85 * Bridge handler responsible for communicating with Lutron hubs that support the LEAP protocol, such as Caseta and
88 * @author Bob Adair - Initial contribution
91 public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessageParserCallbacks {
92 private static final int DEFAULT_RECONNECT_MINUTES = 5;
93 private static final int DEFAULT_HEARTBEAT_MINUTES = 5;
94 private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
96 private static final String STATUS_INITIALIZING = "Initializing";
98 private final Logger logger = LoggerFactory.getLogger(LeapBridgeHandler.class);
100 private @NonNullByDefault({}) LeapBridgeConfig config;
101 private int reconnectInterval;
102 private int heartbeatInterval;
103 private int sendDelay;
105 private @NonNullByDefault({}) SSLSocketFactory sslsocketfactory;
106 private @Nullable SSLSocket sslsocket;
107 private @Nullable BufferedWriter writer;
108 private @Nullable BufferedReader reader;
110 private @NonNullByDefault({}) LeapMessageParser leapMessageParser;
112 private final BlockingQueue<LeapCommand> sendQueue = new LinkedBlockingQueue<>();
114 private @Nullable Future<?> asyncInitializeTask;
116 private @Nullable Thread senderThread;
117 private @Nullable Thread readerThread;
119 private @Nullable ScheduledFuture<?> keepAliveJob;
120 private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
121 private @Nullable ScheduledFuture<?> connectRetryJob;
122 private final Object keepAliveReconnectLock = new Object();
124 private final Map<Integer, Integer> zoneToDevice = new HashMap<>();
125 private final Map<Integer, Integer> deviceToZone = new HashMap<>();
126 private final Object zoneMapsLock = new Object();
128 private @Nullable Map<Integer, List<Integer>> deviceButtonMap;
129 private final Object deviceButtonMapLock = new Object();
131 private volatile boolean deviceDataLoaded = false;
132 private volatile boolean buttonDataLoaded = false;
134 private final Map<Integer, LutronHandler> childHandlerMap = new ConcurrentHashMap<>();
135 private final Map<Integer, OGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
137 private @Nullable LeapDeviceDiscoveryService discoveryService;
139 public void setDiscoveryService(LeapDeviceDiscoveryService discoveryService) {
140 this.discoveryService = discoveryService;
143 public LeapBridgeHandler(Bridge bridge) {
145 leapMessageParser = new LeapMessageParser(this);
149 public Collection<Class<? extends ThingHandlerService>> getServices() {
150 return Collections.singleton(LeapDeviceDiscoveryService.class);
154 public void initialize() {
155 SSLContext sslContext;
157 childHandlerMap.clear();
158 groupHandlerMap.clear();
160 config = getConfigAs(LeapBridgeConfig.class);
161 String keystorePassword = (config.keystorePassword == null) ? "" : config.keystorePassword;
163 String ipAddress = config.ipAddress;
164 if (ipAddress == null || ipAddress.isEmpty()) {
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge address not specified");
169 reconnectInterval = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_MINUTES;
170 heartbeatInterval = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_MINUTES;
171 sendDelay = (config.delay < 0) ? 0 : config.delay;
173 if (config.keystore == null || keystorePassword == null) {
174 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
175 "Keystore/keystore password not configured");
178 try (FileInputStream keystoreInputStream = new FileInputStream(config.keystore)) {
179 logger.trace("Initializing keystore");
180 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
182 keystore.load(keystoreInputStream, keystorePassword.toCharArray());
184 logger.trace("Initializing SSL Context");
185 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
186 kmf.init(keystore, keystorePassword.toCharArray());
188 TrustManager[] trustManagers;
189 if (config.certValidate) {
190 // Use default trust manager which will attempt to validate server certificate from hub
191 TrustManagerFactory tmf = TrustManagerFactory
192 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
194 trustManagers = tmf.getTrustManagers();
196 // Use no-op trust manager which will not verify certificates
197 trustManagers = defineNoOpTrustManager();
200 sslContext = SSLContext.getInstance("TLS");
201 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
203 sslsocketfactory = sslContext.getSocketFactory();
204 } catch (FileNotFoundException e) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Keystore file not found");
207 } catch (CertificateException e) {
208 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Certificate exception");
210 } catch (UnrecoverableKeyException e) {
211 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
212 "Key unrecoverable with supplied password");
214 } catch (KeyManagementException e) {
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Key management exception");
216 logger.debug("Key management exception", e);
218 } catch (KeyStoreException | NoSuchAlgorithmException | IOException e) {
219 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Error initializing keystore");
220 logger.debug("Error initializing keystore", e);
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Connecting");
226 asyncInitializeTask = scheduler.submit(this::connect); // start the async connect task
230 * Return a no-op SSL trust manager which will not verify server or client certificates.
232 private TrustManager[] defineNoOpTrustManager() {
233 return new TrustManager[] { new X509TrustManager() {
235 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
236 logger.debug("Assuming client certificate is valid");
237 if (chain != null && logger.isTraceEnabled()) {
238 for (int cert = 0; cert < chain.length; cert++) {
239 logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
240 logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
241 logger.trace("Serial number {}:", chain[cert].getSerialNumber());
247 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
248 logger.debug("Assuming server certificate is valid");
249 if (chain != null && logger.isTraceEnabled()) {
250 for (int cert = 0; cert < chain.length; cert++) {
251 logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
252 logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
253 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
259 public X509Certificate @Nullable [] getAcceptedIssuers() {
265 private synchronized void connect() {
266 deviceDataLoaded = false;
267 buttonDataLoaded = false;
270 logger.debug("Opening SSL connection to {}:{}", config.ipAddress, config.port);
271 SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(config.ipAddress, config.port);
272 sslsocket.startHandshake();
273 writer = new BufferedWriter(new OutputStreamWriter(sslsocket.getOutputStream()));
274 reader = new BufferedReader(new InputStreamReader(sslsocket.getInputStream()));
275 this.sslsocket = sslsocket;
276 } catch (UnknownHostException e) {
277 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unknown host");
279 } catch (IllegalArgumentException e) {
280 // port out of valid range
281 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid port number");
283 } catch (InterruptedIOException e) {
284 logger.debug("Interrupted while establishing connection");
285 Thread.currentThread().interrupt();
287 } catch (IOException e) {
288 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
289 "Error opening SSL connection. Check log.");
290 logger.info("Error opening SSL connection: {}", e.getMessage());
292 scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
296 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_INITIALIZING);
298 Thread readerThread = new Thread(this::readerThreadJob, "Lutron reader");
299 readerThread.setDaemon(true);
300 readerThread.start();
301 this.readerThread = readerThread;
303 Thread senderThread = new Thread(this::senderThreadJob, "Lutron sender");
304 senderThread.setDaemon(true);
305 senderThread.start();
306 this.senderThread = senderThread;
308 sendCommand(new LeapCommand(Request.getButtonGroups()));
309 queryDiscoveryData();
310 sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus()));
312 logger.debug("Starting keepalive job with interval {}", heartbeatInterval);
313 keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, heartbeatInterval, heartbeatInterval,
318 * Called by connect() and discovery service to request fresh discovery data
320 public void queryDiscoveryData() {
321 sendCommand(new LeapCommand(Request.getDevices()));
322 sendCommand(new LeapCommand(Request.getAreas()));
323 sendCommand(new LeapCommand(Request.getOccupancyGroups()));
326 private void scheduleConnectRetry(long waitMinutes) {
327 logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
328 connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
332 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and clean up.
334 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
335 * connect or reconnect, and true when calling from dispose.
337 private synchronized void disconnect(boolean interruptAll) {
338 logger.debug("Disconnecting");
340 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
341 if (connectRetryJob != null) {
342 connectRetryJob.cancel(true);
344 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
345 if (keepAliveJob != null) {
346 keepAliveJob.cancel(true);
349 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
351 Thread senderThread = this.senderThread;
352 if (senderThread != null && senderThread.isAlive()) {
353 senderThread.interrupt();
356 Thread readerThread = this.readerThread;
357 if (readerThread != null && readerThread.isAlive()) {
358 readerThread.interrupt();
360 SSLSocket sslsocket = this.sslsocket;
361 if (sslsocket != null) {
364 } catch (IOException e) {
365 logger.debug("Error closing SSL socket: {}", e.getMessage());
367 this.sslsocket = null;
369 BufferedReader reader = this.reader;
370 if (reader != null) {
373 } catch (IOException e) {
374 logger.debug("Error closing reader: {}", e.getMessage());
377 BufferedWriter writer = this.writer;
378 if (writer != null) {
381 } catch (IOException e) {
382 logger.debug("Error closing writer: {}", e.getMessage());
386 deviceDataLoaded = false;
387 buttonDataLoaded = false;
390 private synchronized void reconnect() {
391 logger.debug("Attempting to reconnect to the bridge");
392 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "reconnecting");
398 * Method executed by the message sender thread (senderThread)
400 private void senderThreadJob() {
401 logger.debug("Command sender thread started");
403 while (!Thread.currentThread().isInterrupted() && writer != null) {
404 LeapCommand command = sendQueue.take();
405 logger.trace("Sending command {}", command);
408 BufferedWriter writer = this.writer;
409 if (writer != null) {
410 writer.write(command.toString() + "\n");
413 } catch (InterruptedIOException e) {
414 logger.debug("Interrupted while sending");
415 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
416 break; // exit loop and terminate thread
417 } catch (IOException e) {
418 logger.warn("Communication error, will try to reconnect. Error: {}", e.getMessage());
419 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
420 sendQueue.add(command); // Requeue command
422 break; // reconnect() will start a new thread; terminate this one
425 Thread.sleep(sendDelay); // introduce delay to throttle send rate
428 } catch (InterruptedException e) {
429 Thread.currentThread().interrupt();
431 logger.debug("Command sender thread exiting");
436 * Method executed by the message reader thread (readerThread)
438 private void readerThreadJob() {
439 logger.debug("Message reader thread started");
442 BufferedReader reader = this.reader;
443 while (!Thread.interrupted() && reader != null && (msg = reader.readLine()) != null) {
444 leapMessageParser.handleMessage(msg);
447 logger.debug("End of input stream detected");
448 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
450 } catch (InterruptedIOException e) {
451 logger.debug("Interrupted while reading");
452 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
453 } catch (IOException e) {
454 logger.debug("I/O error while reading from stream: {}", e.getMessage());
455 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
456 } catch (RuntimeException e) {
457 logger.warn("Runtime exception in reader thread", e);
458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
460 logger.debug("Message reader thread exiting");
465 * Called if NoContent response received for a buttongroup read request. Creates empty deviceButtonMap.
468 public void handleEmptyButtonGroupDefinition() {
469 logger.debug("No content in button group definition. Creating empty deviceButtonMap.");
470 Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
471 synchronized (deviceButtonMapLock) {
472 this.deviceButtonMap = deviceButtonMap;
473 buttonDataLoaded = true;
479 * Set state to online if offline/initializing and all required initialization info is loaded.
480 * Currently this means device (zone) and button group data.
482 private void checkInitialized() {
483 ThingStatusInfo statusInfo = getThing().getStatusInfo();
484 if (statusInfo.getStatus() == ThingStatus.OFFLINE && STATUS_INITIALIZING.equals(statusInfo.getDescription())) {
485 if (deviceDataLoaded && buttonDataLoaded) {
486 updateStatus(ThingStatus.ONLINE);
492 * Notify child thing handler of a zonelevel update from a received zone status message.
495 public void handleZoneUpdate(ZoneStatus zoneStatus) {
496 logger.trace("Zone: {} level: {}", zoneStatus.getZone(), zoneStatus.level);
497 Integer integrationId = zoneToDevice(zoneStatus.getZone());
499 if (integrationId == null) {
500 logger.debug("Unable to map zone {} to device", zoneStatus.getZone());
503 logger.trace("Zone {} mapped to device id {}", zoneStatus.getZone(), integrationId);
505 // dispatch update to proper thing handler
506 LutronHandler handler = findThingHandler(integrationId);
507 if (handler != null) {
508 if (zoneStatus.fanSpeed != null) {
509 // handle fan controller
510 FanSpeedType fanSpeed = zoneStatus.fanSpeed;
512 handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
513 Integer.valueOf(fanSpeed.speed()).toString());
514 } catch (NumberFormatException e) {
515 logger.warn("Number format exception parsing update");
516 } catch (RuntimeException e) {
517 logger.warn("Runtime exception while processing update");
520 // handle switch/dimmer/shade
522 handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
523 Integer.valueOf(zoneStatus.level).toString());
524 } catch (NumberFormatException e) {
525 logger.warn("Number format exception parsing update");
526 } catch (RuntimeException e) {
527 logger.warn("Runtime exception while processing update");
531 logger.debug("No thing configured for integration ID {}", integrationId);
536 * Notify child group handler of a received occupancy group update.
538 * @param occupancyStatus
542 public void handleGroupUpdate(int groupNumber, String occupancyStatus) {
543 logger.trace("Group {} state update: {}", groupNumber, occupancyStatus);
545 // dispatch update to proper handler
546 OGroupHandler handler = findGroupHandler(groupNumber);
547 if (handler != null) {
549 switch (occupancyStatus) {
551 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
552 GroupCommand.STATE_GRP_OCCUPIED.toString());
555 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
556 GroupCommand.STATE_GRP_UNOCCUPIED.toString());
559 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
560 GroupCommand.STATE_GRP_UNKNOWN.toString());
563 logger.debug("Unexpected occupancy status: {}", occupancyStatus);
566 } catch (NumberFormatException e) {
567 logger.warn("Number format exception parsing update");
568 } catch (RuntimeException e) {
569 logger.warn("Runtime exception while processing update");
572 logger.debug("No group thing configured for group ID {}", groupNumber);
577 public void handleMultipleButtonGroupDefinition(List<ButtonGroup> buttonGroupList) {
578 Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
580 for (ButtonGroup buttonGroup : buttonGroupList) {
581 int parentDevice = buttonGroup.getParentDevice();
582 logger.trace("Found ButtonGroup: {} parent device: {}", buttonGroup.getButtonGroup(), parentDevice);
583 List<Integer> buttonList = buttonGroup.getButtonList();
584 deviceButtonMap.put(parentDevice, buttonList);
586 synchronized (deviceButtonMapLock) {
587 this.deviceButtonMap = deviceButtonMap;
588 buttonDataLoaded = true;
594 public void handleMultipleDeviceDefintion(List<Device> deviceList) {
595 synchronized (zoneMapsLock) {
596 zoneToDevice.clear();
597 deviceToZone.clear();
598 for (Device device : deviceList) {
599 Integer zoneid = device.getZone();
600 Integer deviceid = device.getDevice();
601 logger.trace("Found device: {} id: {} zone: {}", device.name, deviceid, zoneid);
602 if (zoneid > 0 && deviceid > 0) {
603 zoneToDevice.put(zoneid, deviceid);
604 deviceToZone.put(deviceid, zoneid);
606 if (deviceid == 1) { // ID 1 is the bridge
607 setBridgeProperties(device);
611 deviceDataLoaded = true;
614 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
615 if (discoveryService != null) {
616 discoveryService.processDeviceDefinitions(deviceList);
621 public void handleMultipleAreaDefinition(List<Area> areaList) {
622 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
623 if (discoveryService != null) {
624 discoveryService.setAreas(areaList);
629 public void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList) {
630 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
631 if (discoveryService != null) {
632 discoveryService.setOccupancyGroups(oGroupList);
637 public void validMessageReceived(String communiqueType) {
638 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
642 * Set informational bridge properties from the Device entry for the hub/repeater
644 private void setBridgeProperties(Device device) {
645 if (device.getDevice() == 1 && device.repeaterProperties != null) {
646 Map<String, String> properties = editProperties();
647 if (device.name != null) {
648 properties.put(PROPERTY_PRODTYP, device.name);
650 if (device.modelNumber != null) {
651 properties.put(Thing.PROPERTY_MODEL_ID, device.modelNumber);
653 if (device.serialNumber != null) {
654 properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.serialNumber);
656 if (device.firmwareImage != null && device.firmwareImage.firmware != null
657 && device.firmwareImage.firmware.displayName != null) {
658 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.firmwareImage.firmware.displayName);
660 updateProperties(properties);
665 * Queue a LeapCommand for transmission by the sender thread.
667 public void sendCommand(@Nullable LeapCommand command) {
668 if (command != null) {
669 sendQueue.add(command);
674 * Convert a LutronCommand into a LeapCommand and queue it for transmission by the sender thread.
677 public void sendCommand(LutronCommandNew command) {
678 logger.trace("Received request to send Lutron command: {}", command);
679 sendCommand(command.leapCommand(this, deviceToZone(command.getIntegrationId())));
683 * Returns LEAP button number for given integrationID and component. Returns 0 if button number cannot be
686 public int getButton(int integrationID, int component) {
687 synchronized (deviceButtonMapLock) {
688 if (deviceButtonMap != null) {
689 List<Integer> buttonList = deviceButtonMap.get(integrationID);
690 if (buttonList != null && component <= buttonList.size()) {
691 return buttonList.get(component - 1);
693 logger.debug("Could not find button component {} for id {}", component, integrationID);
697 logger.debug("Device to button map not populated");
703 private @Nullable LutronHandler findThingHandler(@Nullable Integer integrationId) {
704 if (integrationId != null) {
705 return childHandlerMap.get(integrationId);
711 private @Nullable OGroupHandler findGroupHandler(int integrationId) {
712 return groupHandlerMap.get(integrationId);
715 private @Nullable Integer zoneToDevice(int zone) {
716 synchronized (zoneMapsLock) {
717 return zoneToDevice.get(zone);
721 private @Nullable Integer deviceToZone(@Nullable Integer device) {
722 if (device == null) {
725 synchronized (zoneMapsLock) {
726 return deviceToZone.get(device);
731 * Executed by keepAliveJob. Sends a LEAP ping request and schedules a reconnect task.
733 private void sendKeepAlive() {
734 logger.trace("Sending keepalive query");
735 sendCommand(new LeapCommand(Request.ping()));
736 // Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
737 reconnectTaskSchedule();
741 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should be
742 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
744 private void reconnectTaskSchedule() {
745 synchronized (keepAliveReconnectLock) {
746 keepAliveReconnectJob = scheduler.schedule(this::keepaliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
752 * Cancels the reconnect task keepAliveReconnectJob.
754 private void reconnectTaskCancel(boolean interrupt) {
755 synchronized (keepAliveReconnectLock) {
756 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
757 if (keepAliveReconnectJob != null) {
758 logger.trace("Canceling scheduled reconnect job.");
759 keepAliveReconnectJob.cancel(interrupt);
760 this.keepAliveReconnectJob = null;
766 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
767 * validMessageReceived() which in turn calls reconnectTaskCancel().
769 private void keepaliveTimeoutExpired() {
770 logger.debug("Keepalive response timeout expired. Initiating reconnect.");
775 public void handleCommand(ChannelUID channelUID, Command command) {
776 if (channelUID.getId().equals(CHANNEL_COMMAND)) {
777 if (command instanceof StringType) {
778 sendCommand(new LeapCommand(command.toString()));
784 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
785 if (childHandler instanceof OGroupHandler) {
786 // We need a different map for group things because the numbering is separate
787 OGroupHandler handler = (OGroupHandler) childHandler;
788 int groupId = handler.getIntegrationId();
789 groupHandlerMap.put(groupId, handler);
790 logger.trace("Registered group handler for ID {}", groupId);
792 LutronHandler handler = (LutronHandler) childHandler;
793 int intId = handler.getIntegrationId();
794 childHandlerMap.put(intId, handler);
795 logger.trace("Registered child handler for ID {}", intId);
800 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
801 if (childHandler instanceof OGroupHandler) {
802 OGroupHandler handler = (OGroupHandler) childHandler;
803 int groupId = handler.getIntegrationId();
804 groupHandlerMap.remove(groupId);
805 logger.trace("Unregistered group handler for ID {}", groupId);
807 LutronHandler handler = (LutronHandler) childHandler;
808 int intId = handler.getIntegrationId();
809 childHandlerMap.remove(intId);
810 logger.trace("Unregistered child handler for ID {}", intId);
815 public void dispose() {
816 Future<?> asyncInitializeTask = this.asyncInitializeTask;
817 if (asyncInitializeTask != null && !asyncInitializeTask.isDone()) {
818 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet