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.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.Arrays;
34 import java.util.Collection;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.List;
39 import java.util.concurrent.BlockingQueue;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.concurrent.Future;
42 import java.util.concurrent.LinkedBlockingQueue;
43 import java.util.concurrent.ScheduledFuture;
44 import java.util.concurrent.TimeUnit;
46 import javax.net.ssl.KeyManagerFactory;
47 import javax.net.ssl.SSLContext;
48 import javax.net.ssl.SSLSocket;
49 import javax.net.ssl.SSLSocketFactory;
50 import javax.net.ssl.TrustManager;
51 import javax.net.ssl.TrustManagerFactory;
52 import javax.net.ssl.X509TrustManager;
54 import org.eclipse.jdt.annotation.NonNullByDefault;
55 import org.eclipse.jdt.annotation.Nullable;
56 import org.openhab.binding.lutron.internal.config.LeapBridgeConfig;
57 import org.openhab.binding.lutron.internal.discovery.LeapDeviceDiscoveryService;
58 import org.openhab.binding.lutron.internal.protocol.FanSpeedType;
59 import org.openhab.binding.lutron.internal.protocol.GroupCommand;
60 import org.openhab.binding.lutron.internal.protocol.LutronCommandNew;
61 import org.openhab.binding.lutron.internal.protocol.OutputCommand;
62 import org.openhab.binding.lutron.internal.protocol.leap.LeapCommand;
63 import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParser;
64 import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParserCallbacks;
65 import org.openhab.binding.lutron.internal.protocol.leap.Request;
66 import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
67 import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
68 import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
69 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
70 import org.openhab.binding.lutron.internal.protocol.leap.dto.Project;
71 import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
72 import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType;
73 import org.openhab.core.library.types.StringType;
74 import org.openhab.core.thing.Bridge;
75 import org.openhab.core.thing.ChannelUID;
76 import org.openhab.core.thing.Thing;
77 import org.openhab.core.thing.ThingStatus;
78 import org.openhab.core.thing.ThingStatusDetail;
79 import org.openhab.core.thing.ThingStatusInfo;
80 import org.openhab.core.thing.binding.ThingHandler;
81 import org.openhab.core.thing.binding.ThingHandlerService;
82 import org.openhab.core.types.Command;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
87 * Bridge handler responsible for communicating with Lutron hubs that support the LEAP protocol, such as Caseta and
90 * @author Bob Adair - Initial contribution
93 public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessageParserCallbacks {
94 private static final int DEFAULT_RECONNECT_MINUTES = 5;
95 private static final int DEFAULT_HEARTBEAT_MINUTES = 5;
96 private static final long KEEPALIVE_TIMEOUT_SECONDS = 30;
98 private static final String STATUS_INITIALIZING = "Initializing";
99 private static final String LUTRON_RADIORA_3_PROJECT = "Lutron RadioRA 3 Project";
101 private final Logger logger = LoggerFactory.getLogger(LeapBridgeHandler.class);
103 private @NonNullByDefault({}) LeapBridgeConfig config;
104 private int reconnectInterval;
105 private int heartbeatInterval;
106 private int sendDelay;
107 private boolean isRadioRA3 = false;
109 private @NonNullByDefault({}) SSLSocketFactory sslsocketfactory;
110 private @Nullable SSLSocket sslsocket;
111 private @Nullable BufferedWriter writer;
112 private @Nullable BufferedReader reader;
114 private @NonNullByDefault({}) LeapMessageParser leapMessageParser;
116 private final BlockingQueue<LeapCommand> sendQueue = new LinkedBlockingQueue<>();
118 private @Nullable Future<?> asyncInitializeTask;
120 private @Nullable Thread senderThread;
121 private @Nullable Thread readerThread;
123 private @Nullable ScheduledFuture<?> keepAliveJob;
124 private @Nullable ScheduledFuture<?> keepAliveReconnectJob;
125 private @Nullable ScheduledFuture<?> connectRetryJob;
126 private final Object keepAliveReconnectLock = new Object();
128 private final Map<Integer, Integer> zoneToDevice = new HashMap<>();
129 private final Map<Integer, Integer> deviceToZone = new HashMap<>();
130 private final Object zoneMapsLock = new Object();
132 private @Nullable Map<Integer, List<Integer>> deviceButtonMap;
133 private final Object deviceButtonMapLock = new Object();
135 private volatile boolean deviceDataLoaded = false;
136 private volatile boolean buttonDataLoaded = false;
138 private final Map<Integer, LutronHandler> childHandlerMap = new ConcurrentHashMap<>();
139 private final Map<Integer, OGroupHandler> groupHandlerMap = new ConcurrentHashMap<>();
141 private @Nullable LeapDeviceDiscoveryService discoveryService;
143 public void setDiscoveryService(LeapDeviceDiscoveryService discoveryService) {
144 this.discoveryService = discoveryService;
147 public LeapBridgeHandler(Bridge bridge) {
149 leapMessageParser = new LeapMessageParser(this);
153 public Collection<Class<? extends ThingHandlerService>> getServices() {
154 return Collections.singleton(LeapDeviceDiscoveryService.class);
158 public void initialize() {
159 SSLContext sslContext;
161 childHandlerMap.clear();
162 groupHandlerMap.clear();
164 config = getConfigAs(LeapBridgeConfig.class);
165 String keystorePassword = (config.keystorePassword == null) ? "" : config.keystorePassword;
167 String ipAddress = config.ipAddress;
168 if (ipAddress == null || ipAddress.isEmpty()) {
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge address not specified");
173 reconnectInterval = (config.reconnect > 0) ? config.reconnect : DEFAULT_RECONNECT_MINUTES;
174 heartbeatInterval = (config.heartbeat > 0) ? config.heartbeat : DEFAULT_HEARTBEAT_MINUTES;
175 sendDelay = (config.delay < 0) ? 0 : config.delay;
177 if (config.keystore == null || keystorePassword == null) {
178 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
179 "Keystore/keystore password not configured");
182 try (FileInputStream keystoreInputStream = new FileInputStream(config.keystore)) {
183 logger.trace("Initializing keystore");
184 KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
186 keystore.load(keystoreInputStream, keystorePassword.toCharArray());
188 logger.trace("Initializing SSL Context");
189 KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
190 kmf.init(keystore, keystorePassword.toCharArray());
192 TrustManager[] trustManagers;
193 if (config.certValidate) {
194 // Use default trust manager which will attempt to validate server certificate from hub
195 TrustManagerFactory tmf = TrustManagerFactory
196 .getInstance(TrustManagerFactory.getDefaultAlgorithm());
198 trustManagers = tmf.getTrustManagers();
200 // Use no-op trust manager which will not verify certificates
201 trustManagers = defineNoOpTrustManager();
204 sslContext = SSLContext.getInstance("TLS");
205 sslContext.init(kmf.getKeyManagers(), trustManagers, null);
207 sslsocketfactory = sslContext.getSocketFactory();
208 } catch (FileNotFoundException e) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Keystore file not found");
211 } catch (CertificateException e) {
212 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Certificate exception");
214 } catch (UnrecoverableKeyException e) {
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
216 "Key unrecoverable with supplied password");
218 } catch (KeyManagementException e) {
219 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Key management exception");
220 logger.debug("Key management exception", e);
222 } catch (KeyStoreException | NoSuchAlgorithmException | IOException e) {
223 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Error initializing keystore");
224 logger.debug("Error initializing keystore", e);
229 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Connecting");
230 asyncInitializeTask = scheduler.submit(this::connect); // start the async connect task
234 * Return a no-op SSL trust manager which will not verify server or client certificates.
236 private TrustManager[] defineNoOpTrustManager() {
237 return new TrustManager[] { new X509TrustManager() {
239 public void checkClientTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
240 logger.debug("Assuming client certificate is valid");
241 if (chain != null && logger.isTraceEnabled()) {
242 for (int cert = 0; cert < chain.length; cert++) {
243 logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
244 logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
245 logger.trace("Serial number {}:", chain[cert].getSerialNumber());
251 public void checkServerTrusted(final X509Certificate @Nullable [] chain, final @Nullable String authType) {
252 logger.debug("Assuming server certificate is valid");
253 if (chain != null && logger.isTraceEnabled()) {
254 for (int cert = 0; cert < chain.length; cert++) {
255 logger.trace("Subject DN: {}", chain[cert].getSubjectDN());
256 logger.trace("Issuer DN: {}", chain[cert].getIssuerDN());
257 logger.trace("Serial number: {}", chain[cert].getSerialNumber());
263 public X509Certificate @Nullable [] getAcceptedIssuers() {
269 private synchronized void connect() {
270 deviceDataLoaded = false;
271 buttonDataLoaded = false;
274 logger.debug("Opening SSL connection to {}:{}", config.ipAddress, config.port);
275 SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket(config.ipAddress, config.port);
276 sslsocket.startHandshake();
277 writer = new BufferedWriter(new OutputStreamWriter(sslsocket.getOutputStream()));
278 reader = new BufferedReader(new InputStreamReader(sslsocket.getInputStream()));
279 this.sslsocket = sslsocket;
280 } catch (UnknownHostException e) {
281 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Unknown host");
283 } catch (IllegalArgumentException e) {
284 // port out of valid range
285 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid port number");
287 } catch (InterruptedIOException e) {
288 logger.debug("Interrupted while establishing connection");
289 Thread.currentThread().interrupt();
291 } catch (IOException e) {
292 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
293 "Error opening SSL connection. Check log.");
294 logger.info("Error opening SSL connection: {}", e.getMessage());
296 scheduleConnectRetry(reconnectInterval); // Possibly a temporary problem. Try again later.
300 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, STATUS_INITIALIZING);
302 Thread readerThread = new Thread(this::readerThreadJob, "Lutron reader");
303 readerThread.setDaemon(true);
304 readerThread.start();
305 this.readerThread = readerThread;
307 Thread senderThread = new Thread(this::senderThreadJob, "Lutron sender");
308 senderThread.setDaemon(true);
309 senderThread.start();
310 this.senderThread = senderThread;
312 sendCommand(new LeapCommand(Request.getProject()));
314 logger.debug("Starting keepalive job with interval {}", heartbeatInterval);
315 keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, heartbeatInterval, heartbeatInterval,
320 * Called by connect() and discovery service to request fresh discovery data
322 public void queryDiscoveryData() {
324 sendCommand(new LeapCommand(Request.getDevices()));
326 sendCommand(new LeapCommand(Request.getDevices(false)));
328 sendCommand(new LeapCommand(Request.getAreas()));
329 sendCommand(new LeapCommand(Request.getOccupancyGroups()));
332 private void scheduleConnectRetry(long waitMinutes) {
333 logger.debug("Scheduling connection retry in {} minutes", waitMinutes);
334 connectRetryJob = scheduler.schedule(this::connect, waitMinutes, TimeUnit.MINUTES);
338 * Disconnect from bridge, cancel retry and keepalive jobs, stop reader and writer threads, and clean up.
340 * @param interruptAll Set if reconnect task should be interrupted if running. Should be false when calling from
341 * connect or reconnect, and true when calling from dispose.
343 private synchronized void disconnect(boolean interruptAll) {
344 logger.debug("Disconnecting");
346 ScheduledFuture<?> connectRetryJob = this.connectRetryJob;
347 if (connectRetryJob != null) {
348 connectRetryJob.cancel(true);
350 ScheduledFuture<?> keepAliveJob = this.keepAliveJob;
351 if (keepAliveJob != null) {
352 keepAliveJob.cancel(true);
355 reconnectTaskCancel(interruptAll); // May be called from keepAliveReconnectJob thread
357 Thread senderThread = this.senderThread;
358 if (senderThread != null && senderThread.isAlive()) {
359 senderThread.interrupt();
362 Thread readerThread = this.readerThread;
363 if (readerThread != null && readerThread.isAlive()) {
364 readerThread.interrupt();
366 SSLSocket sslsocket = this.sslsocket;
367 if (sslsocket != null) {
370 } catch (IOException e) {
371 logger.debug("Error closing SSL socket: {}", e.getMessage());
373 this.sslsocket = null;
375 BufferedReader reader = this.reader;
376 if (reader != null) {
379 } catch (IOException e) {
380 logger.debug("Error closing reader: {}", e.getMessage());
383 BufferedWriter writer = this.writer;
384 if (writer != null) {
387 } catch (IOException e) {
388 logger.debug("Error closing writer: {}", e.getMessage());
392 deviceDataLoaded = false;
393 buttonDataLoaded = false;
396 private synchronized void reconnect() {
397 logger.debug("Attempting to reconnect to the bridge");
398 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "reconnecting");
404 * Method executed by the message sender thread (senderThread)
406 private void senderThreadJob() {
407 logger.debug("Command sender thread started");
409 while (!Thread.currentThread().isInterrupted() && writer != null) {
410 LeapCommand command = sendQueue.take();
411 logger.trace("Sending command {}", command);
414 BufferedWriter writer = this.writer;
415 if (writer != null) {
416 writer.write(command.toString() + "\n");
419 } catch (InterruptedIOException e) {
420 logger.debug("Interrupted while sending");
421 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
422 break; // exit loop and terminate thread
423 } catch (IOException e) {
424 logger.warn("Communication error, will try to reconnect. Error: {}", e.getMessage());
425 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
426 sendQueue.add(command); // Requeue command
428 break; // reconnect() will start a new thread; terminate this one
431 Thread.sleep(sendDelay); // introduce delay to throttle send rate
434 } catch (InterruptedException e) {
435 Thread.currentThread().interrupt();
437 logger.debug("Command sender thread exiting");
442 * Method executed by the message reader thread (readerThread)
444 private void readerThreadJob() {
445 logger.debug("Message reader thread started");
448 BufferedReader reader = this.reader;
449 while (!Thread.interrupted() && reader != null && (msg = reader.readLine()) != null) {
450 leapMessageParser.handleMessage(msg);
453 logger.debug("End of input stream detected");
454 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
456 } catch (InterruptedIOException e) {
457 logger.debug("Interrupted while reading");
458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Interrupted");
459 } catch (IOException e) {
460 logger.debug("I/O error while reading from stream: {}", e.getMessage());
461 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
462 } catch (RuntimeException e) {
463 logger.warn("Runtime exception in reader thread", e);
464 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
466 logger.debug("Message reader thread exiting");
471 * Called if NoContent response received for a buttongroup read request. Creates empty deviceButtonMap.
474 public void handleEmptyButtonGroupDefinition() {
475 logger.debug("No content in button group definition. Creating empty deviceButtonMap.");
476 Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
477 synchronized (deviceButtonMapLock) {
478 this.deviceButtonMap = deviceButtonMap;
479 buttonDataLoaded = true;
485 * Set state to online if offline/initializing and all required initialization info is loaded.
486 * Currently this means device (zone) and button group data.
488 private void checkInitialized() {
489 ThingStatusInfo statusInfo = getThing().getStatusInfo();
490 if (statusInfo.getStatus() == ThingStatus.OFFLINE && STATUS_INITIALIZING.equals(statusInfo.getDescription())) {
491 if (deviceDataLoaded && buttonDataLoaded) {
492 updateStatus(ThingStatus.ONLINE);
498 * Notify child thing handler of a zonelevel update from a received zone status message.
501 public void handleZoneUpdate(ZoneStatus zoneStatus) {
502 logger.trace("Zone: {} level: {}", zoneStatus.getZone(), zoneStatus.level);
503 Integer integrationId = zoneToDevice(zoneStatus.getZone());
505 if (integrationId == null) {
506 logger.debug("Unable to map zone {} to device", zoneStatus.getZone());
509 logger.trace("Zone {} mapped to device id {}", zoneStatus.getZone(), integrationId);
511 // dispatch update to proper thing handler
512 LutronHandler handler = findThingHandler(integrationId);
513 if (handler != null) {
514 if (zoneStatus.fanSpeed != null) {
515 // handle fan controller
516 FanSpeedType fanSpeed = zoneStatus.fanSpeed;
518 handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
519 Integer.valueOf(fanSpeed.speed()).toString());
520 } catch (NumberFormatException e) {
521 logger.warn("Number format exception parsing update");
522 } catch (RuntimeException e) {
523 logger.warn("Runtime exception while processing update");
526 // handle switch/dimmer/shade
528 handler.handleUpdate(LutronCommandType.OUTPUT, OutputCommand.ACTION_ZONELEVEL.toString(),
529 Integer.valueOf(zoneStatus.level).toString());
530 } catch (NumberFormatException e) {
531 logger.warn("Number format exception parsing update");
532 } catch (RuntimeException e) {
533 logger.warn("Runtime exception while processing update");
537 logger.debug("No thing configured for integration ID {}", integrationId);
542 * Notify child group handler of a received occupancy group update.
544 * @param occupancyStatus
548 public void handleGroupUpdate(int groupNumber, String occupancyStatus) {
549 logger.trace("Group {} state update: {}", groupNumber, occupancyStatus);
551 // dispatch update to proper handler
552 OGroupHandler handler = findGroupHandler(groupNumber);
553 if (handler != null) {
555 switch (occupancyStatus) {
557 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
558 GroupCommand.STATE_GRP_OCCUPIED.toString());
561 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
562 GroupCommand.STATE_GRP_UNOCCUPIED.toString());
565 handler.handleUpdate(LutronCommandType.GROUP, GroupCommand.ACTION_GROUPSTATE.toString(),
566 GroupCommand.STATE_GRP_UNKNOWN.toString());
569 logger.debug("Unexpected occupancy status: {}", occupancyStatus);
572 } catch (NumberFormatException e) {
573 logger.warn("Number format exception parsing update");
574 } catch (RuntimeException e) {
575 logger.warn("Runtime exception while processing update");
578 logger.debug("No group thing configured for group ID {}", groupNumber);
583 public void handleMultipleButtonGroupDefinition(List<ButtonGroup> buttonGroupList) {
584 Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
586 for (ButtonGroup buttonGroup : buttonGroupList) {
587 int parentDevice = buttonGroup.getParentDevice();
588 logger.trace("Found ButtonGroup: {} parent device: {}", buttonGroup.getButtonGroup(), parentDevice);
589 List<Integer> buttonList = buttonGroup.getButtonList();
590 deviceButtonMap.put(parentDevice, buttonList);
592 synchronized (deviceButtonMapLock) {
593 this.deviceButtonMap = deviceButtonMap;
594 buttonDataLoaded = true;
600 public void handleDeviceDefinition(Device device) {
601 synchronized (zoneMapsLock) {
602 int deviceId = device.getDevice();
603 int zoneId = device.getZone();
605 if (zoneId > 0 && deviceId > 0) {
606 zoneToDevice.put(zoneId, deviceId);
607 deviceToZone.put(deviceId, zoneId);
610 if (deviceId == 1 || device.isThisDevice) {
611 setBridgeProperties(device);
617 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
618 if (discoveryService != null) {
619 discoveryService.processDeviceDefinitions(Arrays.asList(device));
624 public void handleMultipleDeviceDefinition(List<Device> deviceList) {
625 synchronized (zoneMapsLock) {
626 zoneToDevice.clear();
627 deviceToZone.clear();
628 for (Device device : deviceList) {
629 Integer zoneid = device.getZone();
630 Integer deviceid = device.getDevice();
631 logger.trace("Found device: {} id: {} zone: {}", device.name, deviceid, zoneid);
632 if (zoneid > 0 && deviceid > 0) {
633 zoneToDevice.put(zoneid, deviceid);
634 deviceToZone.put(deviceid, zoneid);
636 if (deviceid == 1 || device.isThisDevice) { // ID 1 is the bridge
637 setBridgeProperties(device);
641 deviceDataLoaded = true;
644 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
645 if (discoveryService != null) {
646 discoveryService.processDeviceDefinitions(deviceList);
651 public void handleMultipleAreaDefinition(List<Area> areaList) {
652 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
653 if (discoveryService != null) {
654 discoveryService.setAreas(areaList);
659 public void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList) {
660 LeapDeviceDiscoveryService discoveryService = this.discoveryService;
661 if (discoveryService != null) {
662 discoveryService.setOccupancyGroups(oGroupList);
667 public void handleProjectDefinition(Project project) {
668 isRadioRA3 = LUTRON_RADIORA_3_PROJECT.equals(project.productType);
670 if (project.masterDeviceList.devices.length > 0 && project.masterDeviceList.devices[0].href != null) {
671 sendCommand(new LeapCommand(Request.getDevices(true)));
674 sendCommand(new LeapCommand(Request.getButtonGroups()));
675 queryDiscoveryData();
678 logger.debug("Caseta Bridge Detected: {}", project.productType);
680 logger.debug("Detected a RadioRA 3 System: {}", project.productType);
681 sendCommand(new LeapCommand(Request.subscribeZoneStatus()));
683 sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus()));
687 public void validMessageReceived(String communiqueType) {
688 reconnectTaskCancel(true); // Got a good message, so cancel reconnect task.
692 * Set informational bridge properties from the Device entry for the hub/repeater
694 private void setBridgeProperties(Device device) {
695 if ((device.getDevice() == 1 && device.repeaterProperties != null) || (device.isThisDevice)) {
696 Map<String, String> properties = editProperties();
697 if (device.name != null) {
698 properties.put(PROPERTY_PRODTYP, device.name);
700 if (device.modelNumber != null) {
701 properties.put(Thing.PROPERTY_MODEL_ID, device.modelNumber);
703 if (device.serialNumber != null) {
704 properties.put(Thing.PROPERTY_SERIAL_NUMBER, device.serialNumber);
706 if (device.firmwareImage != null && device.firmwareImage.firmware != null
707 && device.firmwareImage.firmware.displayName != null) {
708 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, device.firmwareImage.firmware.displayName);
710 updateProperties(properties);
715 * Queue a LeapCommand for transmission by the sender thread.
717 public void sendCommand(@Nullable LeapCommand command) {
718 if (command != null) {
719 sendQueue.add(command);
724 * Convert a LutronCommand into a LeapCommand and queue it for transmission by the sender thread.
727 public void sendCommand(LutronCommandNew command) {
728 logger.trace("Received request to send Lutron command: {}", command);
729 sendCommand(command.leapCommand(this, deviceToZone(command.getIntegrationId())));
733 * Returns LEAP button number for given integrationID and component. Returns 0 if button number cannot be
736 public int getButton(int integrationID, int component) {
737 synchronized (deviceButtonMapLock) {
738 if (deviceButtonMap != null) {
739 List<Integer> buttonList = deviceButtonMap.get(integrationID);
740 if (buttonList != null && component <= buttonList.size()) {
741 return buttonList.get(component - 1);
743 logger.debug("Could not find button component {} for id {}", component, integrationID);
747 logger.debug("Device to button map not populated");
753 private @Nullable LutronHandler findThingHandler(@Nullable Integer integrationId) {
754 if (integrationId != null) {
755 return childHandlerMap.get(integrationId);
761 private @Nullable OGroupHandler findGroupHandler(int integrationId) {
762 return groupHandlerMap.get(integrationId);
765 private @Nullable Integer zoneToDevice(int zone) {
766 synchronized (zoneMapsLock) {
767 return zoneToDevice.get(zone);
771 private @Nullable Integer deviceToZone(@Nullable Integer device) {
772 if (device == null) {
775 synchronized (zoneMapsLock) {
776 return deviceToZone.get(device);
781 * Executed by keepAliveJob. Sends a LEAP ping request and schedules a reconnect task.
783 private void sendKeepAlive() {
784 logger.trace("Sending keepalive query");
785 sendCommand(new LeapCommand(Request.ping()));
786 // Reconnect if no response is received within KEEPALIVE_TIMEOUT_SECONDS.
787 reconnectTaskSchedule();
791 * Schedules the reconnect task keepAliveReconnectJob to execute in KEEPALIVE_TIMEOUT_SECONDS. This should be
792 * cancelled by calling reconnectTaskCancel() if a valid response is received from the bridge.
794 private void reconnectTaskSchedule() {
795 synchronized (keepAliveReconnectLock) {
796 keepAliveReconnectJob = scheduler.schedule(this::keepaliveTimeoutExpired, KEEPALIVE_TIMEOUT_SECONDS,
802 * Cancels the reconnect task keepAliveReconnectJob.
804 private void reconnectTaskCancel(boolean interrupt) {
805 synchronized (keepAliveReconnectLock) {
806 ScheduledFuture<?> keepAliveReconnectJob = this.keepAliveReconnectJob;
807 if (keepAliveReconnectJob != null) {
808 logger.trace("Canceling scheduled reconnect job.");
809 keepAliveReconnectJob.cancel(interrupt);
810 this.keepAliveReconnectJob = null;
816 * Executed by keepAliveReconnectJob if it is not cancelled by the LEAP message parser calling
817 * validMessageReceived() which in turn calls reconnectTaskCancel().
819 private void keepaliveTimeoutExpired() {
820 logger.debug("Keepalive response timeout expired. Initiating reconnect.");
825 public void handleCommand(ChannelUID channelUID, Command command) {
826 if (channelUID.getId().equals(CHANNEL_COMMAND)) {
827 if (command instanceof StringType) {
828 sendCommand(new LeapCommand(command.toString()));
834 public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
835 if (childHandler instanceof OGroupHandler) {
836 // We need a different map for group things because the numbering is separate
837 OGroupHandler handler = (OGroupHandler) childHandler;
838 int groupId = handler.getIntegrationId();
839 groupHandlerMap.put(groupId, handler);
840 logger.trace("Registered group handler for ID {}", groupId);
842 LutronHandler handler = (LutronHandler) childHandler;
843 int intId = handler.getIntegrationId();
844 childHandlerMap.put(intId, handler);
845 logger.trace("Registered child handler for ID {}", intId);
850 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
851 if (childHandler instanceof OGroupHandler) {
852 OGroupHandler handler = (OGroupHandler) childHandler;
853 int groupId = handler.getIntegrationId();
854 groupHandlerMap.remove(groupId);
855 logger.trace("Unregistered group handler for ID {}", groupId);
857 LutronHandler handler = (LutronHandler) childHandler;
858 int intId = handler.getIntegrationId();
859 childHandlerMap.remove(intId);
860 logger.trace("Unregistered child handler for ID {}", intId);
865 public void dispose() {
866 Future<?> asyncInitializeTask = this.asyncInitializeTask;
867 if (asyncInitializeTask != null && !asyncInitializeTask.isDone()) {
868 asyncInitializeTask.cancel(true); // Interrupt async init task if it isn't done yet