diff --git a/bundles/org.openhab.binding.lutron/README.md b/bundles/org.openhab.binding.lutron/README.md index 2c60c2c8e9a..f7bac51c50a 100644 --- a/bundles/org.openhab.binding.lutron/README.md +++ b/bundles/org.openhab.binding.lutron/README.md @@ -144,7 +144,6 @@ Bridge lutron:ipbridge:radiora2 [ ipAddress="192.168.1.2", user="lutron", passwo The leapbridge is an experimental bridge which allows the binding to work with the Caseta Smart Hub (non-Pro version) and the RadioRA 3 Processor. It can also be used to provide additional features, such as support for occupancy groups and device discovery, when used with Caseta Smart Hub Pro or RA2 Select. It uses the LEAP protocol over SSL, which is an undocumented protocol supported by some of Lutron's newer systems. -Note that the LEAP protocol will not notify the bridge of keypad key presses. If you need this useful feature, you should use ipbridge instead. You can use both ipbridge and leapbridge at the same time, but each device should only be configured through one bridge. You should also be aware that LEAP and LIP integration IDs for the same device can be different. diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/BaseKeypadHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/BaseKeypadHandler.java index 5c26e84c0cd..6078de73b92 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/BaseKeypadHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/BaseKeypadHandler.java @@ -55,6 +55,7 @@ public abstract class BaseKeypadHandler extends LutronHandler { protected List cciList = new ArrayList<>(); Map leapButtonMap; + Map leapButtonInverseMap; protected int integrationId; protected String model; @@ -361,6 +362,11 @@ public abstract class BaseKeypadHandler extends LutronHandler { return; } + // LEAP buttons need to be translated back from their index to component id + if (leapButtonInverseMap != null) { + component = leapButtonInverseMap.get(component); + } + ChannelUID channelUID = channelFromComponent(component); if (channelUID != null) { diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java index 8336751d235..0bd9a24760c 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java @@ -30,12 +30,14 @@ import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.AbstractMap.SimpleEntry; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; @@ -55,6 +57,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.config.LeapBridgeConfig; import org.openhab.binding.lutron.internal.discovery.LeapDeviceDiscoveryService; +import org.openhab.binding.lutron.internal.protocol.DeviceCommand; import org.openhab.binding.lutron.internal.protocol.FanSpeedType; import org.openhab.binding.lutron.internal.protocol.GroupCommand; import org.openhab.binding.lutron.internal.protocol.LutronCommandNew; @@ -65,6 +68,7 @@ import org.openhab.binding.lutron.internal.protocol.leap.LeapMessageParserCallba import org.openhab.binding.lutron.internal.protocol.leap.Request; import org.openhab.binding.lutron.internal.protocol.leap.dto.Area; import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup; +import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonStatus; import org.openhab.binding.lutron.internal.protocol.leap.dto.Device; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup; import org.openhab.binding.lutron.internal.protocol.leap.dto.Project; @@ -130,6 +134,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag private final Object zoneMapsLock = new Object(); private @Nullable Map> deviceButtonMap; + private Map buttonToDevice = new HashMap<>(); private final Object deviceButtonMapLock = new Object(); private volatile boolean deviceDataLoaded = false; @@ -475,6 +480,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag logger.debug("No content in button group definition. Creating empty deviceButtonMap."); Map> deviceButtonMap = new HashMap<>(); synchronized (deviceButtonMapLock) { + buttonToDevice.clear(); this.deviceButtonMap = deviceButtonMap; buttonDataLoaded = true; } @@ -582,15 +588,21 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag @Override public void handleMultipleButtonGroupDefinition(List buttonGroupList) { Map> deviceButtonMap = new HashMap<>(); + Map buttonToDevice = new HashMap<>(); for (ButtonGroup buttonGroup : buttonGroupList) { int parentDevice = buttonGroup.getParentDevice(); logger.trace("Found ButtonGroup: {} parent device: {}", buttonGroup.getButtonGroup(), parentDevice); List buttonList = buttonGroup.getButtonList(); deviceButtonMap.put(parentDevice, buttonList); + for (Integer buttonId : buttonList) { + buttonToDevice.put(buttonId, parentDevice); + sendCommand(new LeapCommand(Request.subscribeButtonStatus(buttonId))); + } } synchronized (deviceButtonMapLock) { this.deviceButtonMap = deviceButtonMap; + this.buttonToDevice = buttonToDevice; buttonDataLoaded = true; } checkInitialized(); @@ -683,6 +695,49 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus())); } + /** + * Notify child thing handler of a button update. + */ + @Override + public void handleButtonStatus(ButtonStatus buttonStatus) { + int buttonId = buttonStatus.getButton(); + logger.trace("Button: {} eventType: {}", buttonId, buttonStatus.buttonEvent.eventType); + Entry entry = buttonToDeviceAndIndex(buttonId); + + if (entry == null) { + logger.debug("Unable to map button {} to device", buttonId); + return; + } + int integrationId = entry.getKey(); + int index = entry.getValue(); + logger.trace("Button {} mapped to device id {}, index {}", buttonId, integrationId, index); + + int action; + if ("Press".equals(buttonStatus.buttonEvent.eventType)) { + action = DeviceCommand.ACTION_PRESS; + } else if ("Release".equals(buttonStatus.buttonEvent.eventType)) { + action = DeviceCommand.ACTION_RELEASE; + } else { + logger.warn("Unrecognized button event {} for button {} on device {}", buttonStatus.buttonEvent.eventType, + index, integrationId); + return; + } + + // dispatch update to proper thing handler + LutronHandler handler = findThingHandler(integrationId); + if (handler != null) { + try { + handler.handleUpdate(LutronCommandType.DEVICE, String.valueOf(index), String.valueOf(action)); + } catch (NumberFormatException e) { + logger.warn("Number format exception parsing update"); + } catch (RuntimeException e) { + logger.warn("Runtime exception while processing update"); + } + } else { + logger.debug("No thing configured for integration ID {}", integrationId); + } + } + @Override public void validMessageReceived(String communiqueType) { reconnectTaskCancel(true); // Got a good message, so cancel reconnect task. @@ -777,6 +832,22 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag } } + private @Nullable Entry buttonToDeviceAndIndex(int buttonId) { + synchronized (deviceButtonMapLock) { + Integer deviceId = buttonToDevice.get(buttonId); + if (deviceId == null) { + return null; + } + List buttonList = deviceButtonMap.get(deviceId); + int buttonIndex = buttonList.indexOf(buttonId); + if (buttonIndex == -1) { + return null; + } + + return new SimpleEntry(deviceId, buttonIndex + 1); + } + } + /** * Executed by keepAliveJob. Sends a LEAP ping request and schedules a reconnect task. */ diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/PicoKeypadHandler.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/PicoKeypadHandler.java index 0473cdd4b25..4f2cfd6bde6 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/PicoKeypadHandler.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/PicoKeypadHandler.java @@ -12,6 +12,9 @@ */ package org.openhab.binding.lutron.internal.handler; +import java.util.Map.Entry; +import java.util.stream.Collectors; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.discovery.project.ComponentType; @@ -66,5 +69,7 @@ public class PicoKeypadHandler extends BaseKeypadHandler { leapButtonMap = KeypadConfigPico.LEAPBUTTONS_3BRL; break; } + leapButtonInverseMap = leapButtonMap.entrySet().stream() + .collect(Collectors.toMap(Entry::getValue, Entry::getKey)); } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java index b0d6e96fa89..e146dba6ea7 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java @@ -20,6 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.lutron.internal.protocol.leap.dto.Area; import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup; +import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonStatus; import org.openhab.binding.lutron.internal.protocol.leap.dto.Device; import org.openhab.binding.lutron.internal.protocol.leap.dto.ExceptionDetail; import org.openhab.binding.lutron.internal.protocol.leap.dto.Header; @@ -96,6 +97,7 @@ public class LeapMessageParser { handleReadResponseMessage(message); break; case "UpdateResponse": + handleReadResponseMessage(message); break; case "SubscribeResponse": // Subscribe responses can contain bodies with data @@ -188,6 +190,9 @@ public class LeapMessageParser { case "OneDeviceDefinition": parseOneDeviceDefinition(body); break; + case "OneButtonStatusEvent": + parseOneButtonStatusEvent(body); + break; case "MultipleAreaDefinition": parseMultipleAreaDefinition(body); break; @@ -273,6 +278,16 @@ public class LeapMessageParser { } } + /** + * Parses a OneButtonStatusEvent message body. Calls handleButtonStatusEvent() to dispatch button events. + */ + private void parseOneButtonStatusEvent(JsonObject messageBody) { + ButtonStatus buttonStatus = parseBodySingle(messageBody, "ButtonStatus", ButtonStatus.class); + if (buttonStatus != null) { + callback.handleButtonStatus(buttonStatus); + } + } + /** * Parses a MultipleAreaDefinition message body. */ diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParserCallbacks.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParserCallbacks.java index 3df742fc9ae..af82d5f5353 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParserCallbacks.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParserCallbacks.java @@ -17,6 +17,7 @@ import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.protocol.leap.dto.Area; import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup; +import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonStatus; import org.openhab.binding.lutron.internal.protocol.leap.dto.Device; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup; import org.openhab.binding.lutron.internal.protocol.leap.dto.Project; @@ -49,4 +50,6 @@ public interface LeapMessageParserCallbacks { void handleMultipleAreaDefinition(List areaList); void handleMultipleOccupancyGroupDefinition(List oGroupList); + + void handleButtonStatus(ButtonStatus buttonStatus); } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/Request.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/Request.java index 2a5b4f2eb58..8d5146481d0 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/Request.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/Request.java @@ -154,6 +154,10 @@ public class Request { return request(CommuniqueType.READREQUEST, "/occupancygroup/status"); } + public static String subscribeButtonStatus(int button) { + return request(CommuniqueType.SUBSCRIBEREQUEST, String.format("/button/%d/status/event", button)); + } + public static String subscribeOccupancyGroupStatus() { return request(CommuniqueType.SUBSCRIBEREQUEST, "/occupancygroup/status"); } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonEvent.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonEvent.java new file mode 100644 index 00000000000..a1b353f27e7 --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonEvent.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lutron.internal.protocol.leap.dto; + +import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody; + +import com.google.gson.annotations.SerializedName; + +/** + * LEAP ButtonEvent Object + * + * @author Cody Cutrer - Initial contribution + */ +public class ButtonEvent extends AbstractMessageBody { + @SerializedName("EventType") + public String eventType; // Press, Release + + public ButtonEvent() { + } +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonStatus.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonStatus.java new file mode 100644 index 00000000000..da49e8be156 --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonStatus.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.lutron.internal.protocol.leap.dto; + +import java.util.regex.Pattern; + +import org.openhab.binding.lutron.internal.protocol.leap.AbstractMessageBody; + +import com.google.gson.annotations.SerializedName; + +/** + * LEAP ButtonStatus Object + * + * @author Cody Cutrer - Initial contribution + */ +public class ButtonStatus extends AbstractMessageBody { + public static final Pattern BUTTON_HREF_PATTERN = Pattern.compile("/button/([0-9]+)"); + + @SerializedName("ButtonEvent") + public ButtonEvent buttonEvent; + @SerializedName("Button") + public Href button = new Href(); + + public ButtonStatus() { + } + + public int getButton() { + if (button != null) { + return hrefNumber(BUTTON_HREF_PATTERN, button.href); + } else { + return 0; + } + } +}