diff --git a/bundles/org.openhab.binding.lutron/README.md b/bundles/org.openhab.binding.lutron/README.md index 6e4199f6c..2c60c2c8e 100644 --- a/bundles/org.openhab.binding.lutron/README.md +++ b/bundles/org.openhab.binding.lutron/README.md @@ -10,7 +10,7 @@ It contains support for four different types of Lutron systems via different bri Each is described in a separate section below. -# Lutron RadioRA 2/HomeWorks QS/RA2 Select/Caseta Binding +# Lutron RadioRA 3/ RadioRA 2/HomeWorks QS/RA2 Select/Caseta Binding **Note:** While the Lutron Integration Protocol used by ipbridge in this binding should largely be compatible with other current Lutron systems, it has only been fully tested with RadioRA 2, HomeWorks QS, and Caseta with Smart Bridge Pro. Homeworks QS support is still a work in progress, since not all features/devices are supported yet. @@ -23,7 +23,7 @@ The binding has not been tested with Quantum, QS Standalone, myRoom Plus, or Ath This binding currently supports the following thing types: - **ipbridge** - The Lutron main repeater/processor/hub -- **leapbridge** - Experimental bridge that uses LEAP protocol (Caseta & RA2 Select only) +- **leapbridge** - Experimental bridge that uses LEAP protocol (Caseta, Radio RA3 & RA2 Select only) - **dimmer** - Light dimmer - **switch** - Switch or relay module - **fan** - Fan controller @@ -61,7 +61,7 @@ The experimental leapbridge supports full automated discovery of these systems, Other supported Lutron systems must be configured manually. **Note:** Discovery selects ipbridge for HomeWorks QS, RadioRA 2, RA2 Select, and Caseta Smart Bridge Pro. -It select leapbridge for Caseta Smart Bridge, since only LEAP protocol is supported by this system. +It select leapbridge for Caseta Smart Bridge and Radio RA 3, since only LEAP protocol is supported by these systems. ## Binding Configuration @@ -79,13 +79,14 @@ Two different bridges are now supported by the binding for current Lutron system The LIP protocol is supported by ipbridge while the LEAP protocol is supported by leapbridge. Current systems support one or both protocols as shown below. -|Bridge Device | LIP | LEAP | -|------------------------|-----|------| -|HomeWorks QS Processor | X | | -|RadioRA 2 Main Repeater | X | | -|RA2 Select Main Repeater| X | X | -|Caseta Smart Bridge Pro | X | X | -|Caseta Smart Bridge | | X | +| Bridge Device | LIP | LEAP | +|--------------------------|-----|------| +| HomeWorks QS Processor | X | | +| RadioRA 2 Main Repeater | X | | +| RA2 Select Main Repeater | X | X | +| Caseta Smart Bridge Pro | X | X | +| Caseta Smart Bridge | | X | +| RadioRA 3 Processor | | X | If your system supports only one protocol, then the choice of bridge is easy. If you have a system that supports both protocols, you must decide which you wish to use. @@ -140,7 +141,7 @@ Bridge lutron:ipbridge:radiora2 [ ipAddress="192.168.1.2", user="lutron", passwo #### leapbridge [**experimental**] -The leapbridge is an experimental bridge which allows the binding to work with the Caseta Smart Hub (non-Pro version). +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. diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/discovery/LeapDeviceDiscoveryService.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/discovery/LeapDeviceDiscoveryService.java index 97edabcea..bd3e2583b 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/discovery/LeapDeviceDiscoveryService.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/discovery/LeapDeviceDiscoveryService.java @@ -91,6 +91,11 @@ public class LeapDeviceDiscoveryService extends AbstractDiscoveryService case "RA2SelectMainRepeater": notifyDiscovery(THING_TYPE_VIRTUALKEYPAD, deviceId, label, "model", "Caseta"); break; + case "RadioRa3Processor": + notifyDiscovery(THING_TYPE_VIRTUALKEYPAD, deviceId, label, "model", "RadioRA 3"); + break; + case "MaestroDimmer": + case "SunnataDimmer": case "WallDimmer": case "PlugInDimmer": notifyDiscovery(THING_TYPE_DIMMER, deviceId, label); 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 d02547d55..8347fb4cd 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,6 +30,7 @@ import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -66,6 +67,7 @@ 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.Device; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup; +import org.openhab.binding.lutron.internal.protocol.leap.dto.Project; import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; import org.openhab.binding.lutron.internal.protocol.lip.LutronCommandType; import org.openhab.core.library.types.StringType; @@ -94,6 +96,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag private static final long KEEPALIVE_TIMEOUT_SECONDS = 30; private static final String STATUS_INITIALIZING = "Initializing"; + private static final String LUTRON_RADIORA_3_PROJECT = "Lutron RadioRA 3 Project"; private final Logger logger = LoggerFactory.getLogger(LeapBridgeHandler.class); @@ -101,6 +104,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag private int reconnectInterval; private int heartbeatInterval; private int sendDelay; + private boolean isRadioRA3 = false; private @NonNullByDefault({}) SSLSocketFactory sslsocketfactory; private @Nullable SSLSocket sslsocket; @@ -305,9 +309,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag senderThread.start(); this.senderThread = senderThread; - sendCommand(new LeapCommand(Request.getButtonGroups())); - queryDiscoveryData(); - sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus())); + sendCommand(new LeapCommand(Request.getProject())); logger.debug("Starting keepalive job with interval {}", heartbeatInterval); keepAliveJob = scheduler.scheduleWithFixedDelay(this::sendKeepAlive, heartbeatInterval, heartbeatInterval, @@ -318,7 +320,11 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag * Called by connect() and discovery service to request fresh discovery data */ public void queryDiscoveryData() { - sendCommand(new LeapCommand(Request.getDevices())); + if (!isRadioRA3) { + sendCommand(new LeapCommand(Request.getDevices())); + } else { + sendCommand(new LeapCommand(Request.getDevices(false))); + } sendCommand(new LeapCommand(Request.getAreas())); sendCommand(new LeapCommand(Request.getOccupancyGroups())); } @@ -591,7 +597,31 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag } @Override - public void handleMultipleDeviceDefintion(List deviceList) { + public void handleDeviceDefinition(Device device) { + synchronized (zoneMapsLock) { + int deviceId = device.getDevice(); + int zoneId = device.getZone(); + + if (zoneId > 0 && deviceId > 0) { + zoneToDevice.put(zoneId, deviceId); + deviceToZone.put(deviceId, zoneId); + } + + if (deviceId == 1 || device.isThisDevice) { + setBridgeProperties(device); + } + } + + checkInitialized(); + + LeapDeviceDiscoveryService discoveryService = this.discoveryService; + if (discoveryService != null) { + discoveryService.processDeviceDefinitions(Arrays.asList(device)); + } + } + + @Override + public void handleMultipleDeviceDefinition(List deviceList) { synchronized (zoneMapsLock) { zoneToDevice.clear(); deviceToZone.clear(); @@ -603,7 +633,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag zoneToDevice.put(zoneid, deviceid); deviceToZone.put(deviceid, zoneid); } - if (deviceid == 1) { // ID 1 is the bridge + if (deviceid == 1 || device.isThisDevice) { // ID 1 is the bridge setBridgeProperties(device); } } @@ -633,6 +663,26 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag } } + @Override + public void handleProjectDefinition(Project project) { + isRadioRA3 = LUTRON_RADIORA_3_PROJECT.equals(project.productType); + + if (project.masterDeviceList.devices.length > 0 && project.masterDeviceList.devices[0].href != null) { + sendCommand(new LeapCommand(Request.getDevices(true))); + } + + sendCommand(new LeapCommand(Request.getButtonGroups())); + queryDiscoveryData(); + + if (!isRadioRA3) { + logger.debug("Caseta Bridge Detected: {}", project.productType); + } else { + logger.debug("Detected a RadioRA 3 System: {}", project.productType); + sendCommand(new LeapCommand(Request.subscribeZoneStatus())); + } + sendCommand(new LeapCommand(Request.subscribeOccupancyGroupStatus())); + } + @Override public void validMessageReceived(String communiqueType) { reconnectTaskCancel(true); // Got a good message, so cancel reconnect task. @@ -642,7 +692,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag * Set informational bridge properties from the Device entry for the hub/repeater */ private void setBridgeProperties(Device device) { - if (device.getDevice() == 1 && device.repeaterProperties != null) { + if ((device.getDevice() == 1 && device.repeaterProperties != null) || (device.isThisDevice)) { Map properties = editProperties(); if (device.name != null) { properties.put(PROPERTY_PRODTYP, device.name); 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 5bcbae934..5fa4b8264 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 @@ -25,6 +25,7 @@ import org.openhab.binding.lutron.internal.protocol.leap.dto.ExceptionDetail; import org.openhab.binding.lutron.internal.protocol.leap.dto.Header; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroupStatus; +import org.openhab.binding.lutron.internal.protocol.leap.dto.Project; import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -181,6 +182,12 @@ public class LeapMessageParser { case "OneZoneStatus": parseOneZoneStatus(body); break; + case "OneProjectDefinition": + parseOneProjectDefinition(body); + break; + case "OneDeviceDefinition": + parseOneDeviceDefinition(body); + break; case "MultipleAreaDefinition": parseMultipleAreaDefinition(body); break; @@ -198,6 +205,9 @@ public class LeapMessageParser { break; case "MultipleVirtualButtonDefinition": break; + case "MultipleZoneStatus": + parseMultipleZoneStatus(body); + break; default: logger.debug("Unknown MessageBodyType received: {}", messageBodyType); break; @@ -297,13 +307,21 @@ public class LeapMessageParser { } } + private void parseMultipleZoneStatus(JsonObject messageBody) { + List statusList = parseBodyMultiple(messageBody, "ZoneStatuses", ZoneStatus.class); + for (ZoneStatus status : statusList) { + logger.debug("Setting zone {} to level: {}", status.href, status.level); + callback.handleZoneUpdate(status); + } + } + /** * Parses a MultipleDeviceDefinition message body and loads the zoneToDevice and deviceToZone maps. Also passes the * device data on to the discovery service and calls setBridgeProperties() with the hub's device entry. */ private void parseMultipleDeviceDefinition(JsonObject messageBody) { List deviceList = parseBodyMultiple(messageBody, "Devices", Device.class); - callback.handleMultipleDeviceDefintion(deviceList); + callback.handleMultipleDeviceDefinition(deviceList); } /** @@ -313,4 +331,18 @@ public class LeapMessageParser { List buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class); callback.handleMultipleButtonGroupDefinition(buttonGroupList); } + + private void parseOneProjectDefinition(JsonObject messageBody) { + Project project = parseBodySingle(messageBody, "Project", Project.class); + if (project != null) { + callback.handleProjectDefinition(project); + } + } + + private void parseOneDeviceDefinition(JsonObject messageBody) { + Device device = parseBodySingle(messageBody, "Device", Device.class); + if (device != null) { + callback.handleDeviceDefinition(device); + } + } } 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 2018d9283..4f749769e 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 @@ -19,6 +19,7 @@ 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.Device; import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup; +import org.openhab.binding.lutron.internal.protocol.leap.dto.Project; import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; /** @@ -29,6 +30,10 @@ import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; @NonNullByDefault public interface LeapMessageParserCallbacks { + void handleProjectDefinition(Project project); + + void handleDeviceDefinition(Device device); + void validMessageReceived(String communiqueType); void handleEmptyButtonGroupDefinition(); @@ -39,7 +44,7 @@ public interface LeapMessageParserCallbacks { void handleMultipleButtonGroupDefinition(List buttonGroupList); - void handleMultipleDeviceDefintion(List deviceList); + void handleMultipleDeviceDefinition(List deviceList); void handleMultipleAreaDefinition(List areaList); 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 f8681abc0..8dfb5320a 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 @@ -108,7 +108,22 @@ public class Request { } public static String getDevices() { - return request(CommuniqueType.READREQUEST, "/device"); + return getDevices(""); + } + + public static String getDevices(boolean thisDevice) { + String url = String.format("where=IsThisDevice:%s", (thisDevice) ? "true" : "false"); + + return getDevices(url); + } + + public static String getDevices(String predicate) { + String url = "/device"; + if (!predicate.isEmpty()) { + url = String.format("%s?%s", url, predicate); + } + + return request(CommuniqueType.READREQUEST, url); } public static String getVirtualButtons() { @@ -119,6 +134,10 @@ public class Request { return request(CommuniqueType.READREQUEST, BUTTON_GROUP_URL); } + public static String getProject() { + return request(CommuniqueType.READREQUEST, "/project"); + } + public static String getAreas() { return request(CommuniqueType.READREQUEST, "/area"); } @@ -138,4 +157,8 @@ public class Request { public static String subscribeOccupancyGroupStatus() { return request(CommuniqueType.SUBSCRIBEREQUEST, "/occupancygroup/status"); } + + public static String subscribeZoneStatus() { + return request(CommuniqueType.SUBSCRIBEREQUEST, "/zone/status"); + } } diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Device.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Device.java index 646b0b3df..4c1c3d43e 100644 --- a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Device.java +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Device.java @@ -69,11 +69,14 @@ public class Device extends AbstractMessageBody { @SerializedName("FirmwareImage") public FirmwareImage firmwareImage; + @SerializedName("IsThisDevice") + public boolean isThisDevice; + public class FirmwareImage { @SerializedName("Firmware") public Firmware firmware; @SerializedName("Installed") - public Installed installed; + public ProjectTimestamp installed; } public class Firmware { @@ -81,23 +84,6 @@ public class Device extends AbstractMessageBody { public String displayName; } - public class Installed { - @SerializedName("Year") - public int year; - @SerializedName("Month") - public int month; - @SerializedName("Day") - public int day; - @SerializedName("Hour") - public int hour; - @SerializedName("Minute") - public int minute; - @SerializedName("Second") - public int second; - @SerializedName("Utc") - public String utc; - } - public class RepeaterProperties { @SerializedName("IsRepeater") public boolean isRepeater; diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Project.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Project.java new file mode 100644 index 000000000..f68b6d8ec --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Project.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2023 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 Project Object + * + * @author Peter Wojciechowski - Initial contribution + */ +public class Project extends AbstractMessageBody { + @SerializedName("href") + public String href; + + @SerializedName("Name") + public String name; + + @SerializedName("ProductType") + public String productType; + + @SerializedName("MasterDeviceList") + public MasterDeviceList masterDeviceList; + + @SerializedName("Contacts") + public Href[] contacts; + + @SerializedName("TimeclockEventRules") + public Href timeclockEventRules; + + @SerializedName("ProjectModifiedTimestamp") + public ProjectTimestamp projectModifiedTimestamp; + + public class MasterDeviceList { + public static final Pattern DEVICE_HREF_PATTERN = Pattern.compile("/device/([0-9]+)"); + + public int getDeviceIdFromHref(int deviceIndex) { + if (devices.length == 0) { + return 0; + } + + return hrefNumber(DEVICE_HREF_PATTERN, devices[deviceIndex].href); + } + + @SerializedName("Devices") + public Href[] devices; + } +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ProjectTimestamp.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ProjectTimestamp.java new file mode 100644 index 000000000..122089816 --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ProjectTimestamp.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2023 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 com.google.gson.annotations.SerializedName; + +/** + * LEAP ProjectTimestamp Object + * + * @author Peter Wojciechowski - Initial contribution + */ +public class ProjectTimestamp { + @SerializedName("Year") + public int year; + @SerializedName("Month") + public int month; + @SerializedName("Day") + public int day; + @SerializedName("Hour") + public int hour; + @SerializedName("Minute") + public int minute; + @SerializedName("Second") + public int second; + @SerializedName("Utc") + public String utc; +}