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 618c244be11..64833b2ca5f 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 @@ -68,11 +68,24 @@ public class LeapDeviceDiscoveryService extends AbstractThingHandlerDiscoverySer thingHandler.queryDiscoveryData(); } - public void processDeviceDefinitions(List deviceList) { + public void processDeviceDefinitions(List deviceList, Map zoneIdToName, + Map areaIdToName) { for (Device device : deviceList) { - // Integer zoneid = device.getZone(); + Integer zoneId = device.getZone(); Integer deviceId = device.getDevice(); + Integer areaId = device.getArea(); + String label = device.getFullyQualifiedName(); + // RA3 doesn't provide a name for the above instead you + // need to get the Area and Zone name to have it make sense + if (label.isEmpty()) { + String areaName = areaIdToName.getOrDefault(areaId, ""); + if (areaName.isEmpty()) { + label = zoneIdToName.getOrDefault(zoneId, ""); + } else { + label = String.format("%s - %s", areaName, zoneIdToName.getOrDefault(zoneId, "")); + } + } if (deviceId > 0) { logger.debug("Discovered device: {} type: {} id: {}", label, device.deviceType, deviceId); if (device.deviceType != null) { @@ -117,6 +130,11 @@ public class LeapDeviceDiscoveryService extends AbstractThingHandlerDiscoverySer case "QsWirelessShade": notifyDiscovery(THING_TYPE_SHADE, deviceId, label); break; + case "RPSWallMountedOccupancySensor": + // TODO: Handle ra3 OccupancySensors, will need to get area + // that the sensor is associated with to get at the sensor + // status + break; case "RPSOccupancySensor": // Don't discover sensors. Using occupancy groups instead. break; 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 c8f0c7d60fc..63de0d97e98 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 @@ -72,6 +72,7 @@ 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; +import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneExpanded; 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; @@ -133,6 +134,11 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag private final Map deviceToZone = new HashMap<>(); private final Object zoneMapsLock = new Object(); + private final Object zoneIdToNameLock = new Object(); + private final Map zoneIdToName = new HashMap<>(); + public final Object areaIdToNameLock = new Object(); + public final Map areaIdToName = new HashMap<>(); + private @Nullable Map> deviceButtonMap; private Map buttonToDevice = new HashMap<>(); private final Object deviceButtonMapLock = new Object(); @@ -325,12 +331,14 @@ 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.getAreas())); + if (!isRadioRA3) { sendCommand(new LeapCommand(Request.getDevices())); } else { + sendCommand(new LeapCommand(Request.getZoneStatuses())); sendCommand(new LeapCommand(Request.getDevices(false))); } - sendCommand(new LeapCommand(Request.getAreas())); sendCommand(new LeapCommand(Request.getOccupancyGroups())); } @@ -628,7 +636,7 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag LeapDeviceDiscoveryService discoveryService = this.discoveryService; if (discoveryService != null) { - discoveryService.processDeviceDefinitions(Arrays.asList(device)); + discoveryService.processDeviceDefinitions(Arrays.asList(device), zoneIdToName, areaIdToName); } } @@ -655,12 +663,36 @@ public class LeapBridgeHandler extends LutronBridgeHandler implements LeapMessag LeapDeviceDiscoveryService discoveryService = this.discoveryService; if (discoveryService != null) { - discoveryService.processDeviceDefinitions(deviceList); + discoveryService.processDeviceDefinitions(deviceList, zoneIdToName, areaIdToName); + } + } + + @Override + public void handleMultipleZoneExpandedUpdate(List expandedZones) { + synchronized (zoneIdToNameLock) { + zoneIdToName.clear(); + for (ZoneExpanded zone : expandedZones) { + int zoneId = zone.zone.getZone(); + if (zoneId > 0) { + zoneIdToName.put(zoneId, zone.zone.name); + } + } } } @Override public void handleMultipleAreaDefinition(List areaList) { + synchronized (areaIdToNameLock) { + areaIdToName.clear(); + for (Area area : areaList) { + int areaId = area.getArea(); + if (areaId > 0) { + logger.debug("area[{}] == {}", areaId, area.name); + areaIdToName.put(areaId, area.name); + } + } + } + LeapDeviceDiscoveryService discoveryService = this.discoveryService; if (discoveryService != null) { discoveryService.setAreas(areaList); 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 f60a1e5d6cf..5262c7369be 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 @@ -27,6 +27,7 @@ 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.ZoneExpanded; import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -213,6 +214,9 @@ public class LeapMessageParser { case "MultipleZoneStatus": parseMultipleZoneStatus(body); break; + case "MultipleZoneExpandedStatus": + parseMultipleZoneExpandedStatus(body); + break; default: logger.debug("Unknown MessageBodyType received: {}", messageBodyType); break; @@ -330,6 +334,12 @@ public class LeapMessageParser { } } + private void parseMultipleZoneExpandedStatus(JsonObject messageBody) { + List zoneExpandedList = parseBodyMultiple(messageBody, "ZoneExpandedStatuses", + ZoneExpanded.class); + callback.handleMultipleZoneExpandedUpdate(zoneExpandedList); + } + /** * 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. 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 73227f1cea8..061853f2517 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 @@ -21,6 +21,7 @@ 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; +import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneExpanded; import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus; /** @@ -41,6 +42,8 @@ public interface LeapMessageParserCallbacks { void handleZoneUpdate(ZoneStatus zoneStatus); + void handleMultipleZoneExpandedUpdate(List expandedZones); + void handleGroupUpdate(int groupNumber, String occupancyStatus); void handleMultipleButtonGroupDefinition(List buttonGroupList); 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 0f6f39f48b6..97d0928fb73 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 @@ -14,6 +14,9 @@ package org.openhab.binding.lutron.internal.protocol.leap; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.lutron.internal.protocol.FanSpeedType; +import org.openhab.binding.lutron.internal.protocol.leap.dto.LeapRequest; + +import com.google.gson.Gson; /** * Contains static methods for constructing LEAP messages @@ -99,8 +102,13 @@ public class Request { } public static String request(CommuniqueType cType, String url) { - String request = "{\"CommuniqueType\": \"%s\",\"Header\": {\"Url\": \"%s\"}}"; - return String.format(request, cType.toString(), url); + LeapRequest leapRequest = new LeapRequest(); + leapRequest.communiqueType = cType.toString(); + leapRequest.header = new LeapRequest.Header(); + leapRequest.header.url = url; + + Gson gson = new Gson(); + return gson.toJson(leapRequest); } public static String ping() { @@ -150,6 +158,11 @@ public class Request { return request(CommuniqueType.READREQUEST, String.format("/zone/%d/status", zone)); } + public static String getZoneStatuses() { + return request(CommuniqueType.READREQUEST, + "/zone/status/expanded?where=Zone.ControlType:\"Dimmed\"|\"Switched\"|\"CCO\"|\"Shade\""); + } + public static String getOccupancyGroupStatus() { return request(CommuniqueType.READREQUEST, "/occupancygroup/status"); } @@ -165,4 +178,8 @@ public class Request { public static String subscribeZoneStatus() { return request(CommuniqueType.SUBSCRIBEREQUEST, "/zone/status"); } + + public static String subscribeAreaStatus() { + return request(CommuniqueType.SUBSCRIBEREQUEST, "/area/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 11f789677f0..e4e9e54eded 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 @@ -26,6 +26,7 @@ import com.google.gson.annotations.SerializedName; public class Device extends AbstractMessageBody { public static final Pattern DEVICE_HREF_PATTERN = Pattern.compile("/device/([0-9]+)"); private static final Pattern ZONE_HREF_PATTERN = Pattern.compile("/zone/([0-9]+)"); + private static final Pattern AREA_HREF_PATTERN = Pattern.compile("/area/([0-9]+)"); @SerializedName("href") public String href; @@ -108,6 +109,14 @@ public class Device extends AbstractMessageBody { } } + public int getArea() { + if (associatedArea != null && !associatedArea.href.isEmpty()) { + return hrefNumber(AREA_HREF_PATTERN, associatedArea.href); + } else { + return 0; + } + } + public String getFullyQualifiedName() { if (fullyQualifiedName != null && fullyQualifiedName.length > 0) { return String.join(" ", fullyQualifiedName); diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/LeapRequest.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/LeapRequest.java new file mode 100644 index 00000000000..def926e5883 --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/LeapRequest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2025 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; + +/** + * LeapRequest object + * + * @author Peter J Wojciechowski - initial + */ +public class LeapRequest { + @SerializedName("CommuniqueType") + public String communiqueType = ""; + @SerializedName("Header") + public Header header; + @SerializedName("Body") + public Body body; + + public static class Body { + @SerializedName("Command") + public Command command; + } + + public static class Command { + @SerializedName("CommandType") + public String commandType; + @SerializedName("Parameter") + public CommandParameter commandParameter; + + public static class CommandParameter { + @SerializedName("Type") + public String type; + @SerializedName("Value") + public String value; + } + } + + public static class Header { + @SerializedName("Url") + public String url; + } +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Zone.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Zone.java new file mode 100644 index 00000000000..62131e8afe5 --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/Zone.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2010-2025 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 Zone Object + * + * @author Peter J Wojciechowski - Initial contribution + */ + +public class Zone extends AbstractMessageBody { + public static final Pattern ZONE_HREF_PATTERN = Pattern.compile("/zone/([0-9]+)"); + + @SerializedName("href") + public String href = ""; + @SerializedName("XID") + public String xid = ""; + @SerializedName("Name") + public String name = ""; + @SerializedName("AvailableControlTypes") + public String[] controlTypes = {}; + @SerializedName("Category") + public ZoneCategory zoneCategory = new ZoneCategory(); + @SerializedName("AssociatedArea") + public Href associatedArea = new Href(); + @SerializedName("SortOrder") + public int sortOrder; + @SerializedName("ControlType") + public String controlType = ""; + + public int getZone() { + if (href != null) { + return hrefNumber(ZONE_HREF_PATTERN, href); + } else { + return 0; + } + } + + public class ZoneCategory { + @SerializedName("Type") + public String type = ""; + @SerializedName("IsLight") + public boolean isLight = false; + } +} diff --git a/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ZoneExpanded.java b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ZoneExpanded.java new file mode 100644 index 00000000000..e06629472ef --- /dev/null +++ b/bundles/org.openhab.binding.lutron/src/main/java/org/openhab/binding/lutron/internal/protocol/leap/dto/ZoneExpanded.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010-2025 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; + +/** + * ZoneExpanded object + * + * @author Peter J Wojciechowski - initial + */ +public class ZoneExpanded extends AbstractMessageBody { + @SerializedName("href") + public String href; + @SerializedName("Level") + public int level; + @SerializedName("StatusAccuracy") + public String statusAccuracy; + @SerializedName("Zone") + public Zone zone; +}