[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 <cody@cutrer.us>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Cody Cutrer 2024-04-05 13:53:06 -06:00 committed by Ciprian Pascu
parent 94bcec917e
commit 9d68e719b4
9 changed files with 178 additions and 1 deletions

View 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.

View 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) {

View 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.
*/

View 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));
}
}

View 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.
*/

View 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);
}

View 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");
}

View File

@ -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() {
}
}

View File

@ -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;
}
}
}