From aebbbdffc45ceb85fd6f15eccc624fbf04c41fa2 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Fri, 5 Apr 2024 13:53:06 -0600 Subject: [PATCH] [lutron] Implement button press notifications for Picos from LEAP (#16550) * [lutron] implement button press notifications for Picos from LEAP * reverse equality check for null safety Signed-off-by: Cody Cutrer --- bundles/org.openhab.binding.lutron/README.md | 1 - .../internal/handler/BaseKeypadHandler.java | 6 ++ .../internal/handler/LeapBridgeHandler.java | 71 +++++++++++++++++++ .../internal/handler/PicoKeypadHandler.java | 5 ++ .../protocol/leap/LeapMessageParser.java | 15 ++++ .../leap/LeapMessageParserCallbacks.java | 3 + .../internal/protocol/leap/Request.java | 4 ++ .../protocol/leap/dto/ButtonEvent.java | 30 ++++++++ .../protocol/leap/dto/ButtonStatus.java | 44 ++++++++++++ 9 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonEvent.java create mode 100644 bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonStatus.java diff --git a/bundles/org.openhab.binding.lutron/README.md b/bundles/org.openhab.binding.lutron/README.md index 2c60c2c8e9..f7bac51c50 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 5c26e84c0c..6078de73b9 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 8336751d23..0bd9a24760 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 0473cdd4b2..4f2cfd6bde 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 b0d6e96fa8..e146dba6ea 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 3df742fc9a..af82d5f535 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 2a5b4f2eb5..8d5146481d 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 0000000000..a1b353f27e --- /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 0000000000..da49e8be15 --- /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; + } + } +} -- 2.47.3