mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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>
This commit is contained in:
parent
23502fcb9b
commit
aebbbdffc4
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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() {
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user