]> git.basschouten.com Git - openhab-addons.git/commitdiff
[lutron] Implement button press notifications for Picos from LEAP (#16550)
authorCody Cutrer <cody@cutrer.us>
Fri, 5 Apr 2024 19:53:06 +0000 (13:53 -0600)
committerGitHub <noreply@github.com>
Fri, 5 Apr 2024 19:53:06 +0000 (21:53 +0200)
* [lutron] implement button press notifications for Picos from LEAP
* reverse equality check for null safety

Signed-off-by: Cody Cutrer <cody@cutrer.us>
bundles/org.openhab.binding.lutron/README.md
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/BaseKeypadHandler.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/LeapBridgeHandler.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/handler/PicoKeypadHandler.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParser.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/LeapMessageParserCallbacks.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/Request.java
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonEvent.java [new file with mode: 0644]
bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ButtonStatus.java [new file with mode: 0644]

index 2c60c2c8e9a0944aaa84436db9287b1ec70ff6c9..f7bac51c50aa8efc5bbd0357c3b4b3e31335ebb0 100644 (file)
@@ -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.
index 5c26e84c0cdf11a3fa745a1e384f86348b40e165..6078de73b925d5d99e1f53ad289c4fdbf3ecf16e 100644 (file)
@@ -55,6 +55,7 @@ public abstract class BaseKeypadHandler extends LutronHandler {
     protected List<KeypadComponent> cciList = new ArrayList<>();
 
     Map<Integer, Integer> leapButtonMap;
+    Map<Integer, Integer> 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) {
index 8336751d2358d7e2fd9459b25fc357ef6d6c7eab..0bd9a24760cb85809d5076dde33797c8e6afc8d7 100644 (file)
@@ -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<Integer, List<Integer>> deviceButtonMap;
+    private Map<Integer, Integer> 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<Integer, List<Integer>> 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<ButtonGroup> buttonGroupList) {
         Map<Integer, List<Integer>> deviceButtonMap = new HashMap<>();
+        Map<Integer, Integer> buttonToDevice = new HashMap<>();
 
         for (ButtonGroup buttonGroup : buttonGroupList) {
             int parentDevice = buttonGroup.getParentDevice();
             logger.trace("Found ButtonGroup: {} parent device: {}", buttonGroup.getButtonGroup(), parentDevice);
             List<Integer> 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<Integer, Integer> 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<Integer, Integer> buttonToDeviceAndIndex(int buttonId) {
+        synchronized (deviceButtonMapLock) {
+            Integer deviceId = buttonToDevice.get(buttonId);
+            if (deviceId == null) {
+                return null;
+            }
+            List<Integer> 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.
      */
index 0473cdd4b25d897f9ddb45798b9557033f679ad3..4f2cfd6bde67c47156f3147c3d73d54ce0f40c9b 100644 (file)
@@ -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));
     }
 }
index b0d6e96fa898ae5abd0f5c50d26bf4cfbcab35c0..e146dba6ea7fadaf2eb6e0d1b250749ebd6add18 100644 (file)
@@ -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.
      */
index 3df742fc9aeb7655851e6f6d4baf3229437a6d86..af82d5f53537cb833c217c4ba61cbfda6cd89769 100644 (file)
@@ -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<Area> areaList);
 
     void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList);
+
+    void handleButtonStatus(ButtonStatus buttonStatus);
 }
index 2a5b4f2eb58e1968a161b6d9aff73f6993406155..8d5146481d080dc68353586512b99686fee87020 100644 (file)
@@ -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 (file)
index 0000000..a1b353f
--- /dev/null
@@ -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 (file)
index 0000000..da49e8b
--- /dev/null
@@ -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;
+        }
+    }
+}