Skip to content

Commit

Permalink
[lutron] Implement button press notifications for Picos from LEAP (#1…
Browse files Browse the repository at this point in the history
…6550)

* [lutron] implement button press notifications for Picos from LEAP
* reverse equality check for null safety

Signed-off-by: Cody Cutrer <cody@cutrer.us>
  • Loading branch information
ccutrer authored Apr 5, 2024
1 parent 23502fc commit aebbbdf
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 1 deletion.
1 change: 0 additions & 1 deletion bundles/org.openhab.binding.lutron/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -361,6 +362,11 @@ public void handleUpdate(LutronCommandType type, String... parameters) {
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@
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;
Expand All @@ -55,6 +57,7 @@
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;
Expand All @@ -65,6 +68,7 @@
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -475,6 +480,7 @@ public void handleEmptyButtonGroupDefinition() {
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;
}
Expand Down Expand Up @@ -582,15 +588,21 @@ public void handleGroupUpdate(int groupNumber, String occupancyStatus) {
@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();
Expand Down Expand Up @@ -683,6 +695,49 @@ public void handleProjectDefinition(Project project) {
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.
Expand Down Expand Up @@ -777,6 +832,22 @@ public int getButton(int integrationID, int component) {
}
}

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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -66,5 +69,7 @@ protected void configureComponents(@Nullable String model) {
leapButtonMap = KeypadConfigPico.LEAPBUTTONS_3BRL;
break;
}
leapButtonInverseMap = leapButtonMap.entrySet().stream()
.collect(Collectors.toMap(Entry::getValue, Entry::getKey));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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;
Expand Down Expand Up @@ -96,6 +97,7 @@ public void handleMessage(String msg) {
handleReadResponseMessage(message);
break;
case "UpdateResponse":
handleReadResponseMessage(message);
break;
case "SubscribeResponse":
// Subscribe responses can contain bodies with data
Expand Down Expand Up @@ -188,6 +190,9 @@ private void handleReadResponseMessage(JsonObject message) {
case "OneDeviceDefinition":
parseOneDeviceDefinition(body);
break;
case "OneButtonStatusEvent":
parseOneButtonStatusEvent(body);
break;
case "MultipleAreaDefinition":
parseMultipleAreaDefinition(body);
break;
Expand Down Expand Up @@ -273,6 +278,16 @@ private void parseOneZoneStatus(JsonObject messageBody) {
}
}

/**
* 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.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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;
Expand Down Expand Up @@ -49,4 +50,6 @@ public interface LeapMessageParserCallbacks {
void handleMultipleAreaDefinition(List<Area> areaList);

void handleMultipleOccupancyGroupDefinition(List<OccupancyGroup> oGroupList);

void handleButtonStatus(ButtonStatus buttonStatus);
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ public static String getOccupancyGroupStatus() {
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");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}

0 comments on commit aebbbdf

Please sign in to comment.