2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.lutron.internal.protocol.leap;
15 import java.util.LinkedList;
16 import java.util.List;
17 import java.util.Objects;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
22 import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
23 import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
24 import org.openhab.binding.lutron.internal.protocol.leap.dto.ExceptionDetail;
25 import org.openhab.binding.lutron.internal.protocol.leap.dto.Header;
26 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
27 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroupStatus;
28 import org.openhab.binding.lutron.internal.protocol.leap.dto.Project;
29 import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 import com.google.gson.Gson;
34 import com.google.gson.GsonBuilder;
35 import com.google.gson.JsonArray;
36 import com.google.gson.JsonElement;
37 import com.google.gson.JsonObject;
38 import com.google.gson.JsonParseException;
39 import com.google.gson.JsonParser;
40 import com.google.gson.JsonSyntaxException;
43 * Class responsible for parsing incoming LEAP messages. Calls back to an object implementing the
44 * LeapMessageParserCallbacks interface.
46 * Thanks to the authors of the pylutron-caseta Python API (github.com/gurumitts/pylutron-caseta), which I used as a
47 * reference when first researching the LEAP protocol.
49 * @author Bob Adair - Initial contribution
52 public class LeapMessageParser {
53 private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
55 private final Gson gson;
56 private final LeapMessageParserCallbacks callback;
59 * LeapMessageParser Constructor
61 * @param callback Object implementing the LeapMessageParserCallbacks interface
63 public LeapMessageParser(LeapMessageParserCallbacks callback) {
64 gson = new GsonBuilder().create();
65 this.callback = callback;
69 * Parse and process a LEAP protocol message
71 * @param msg String containing the LEAP message
73 public void handleMessage(String msg) {
74 if ("".equals(msg.trim())) {
75 return; // Ignore empty lines
77 logger.trace("Received message: {}", msg);
80 JsonObject message = (JsonObject) JsonParser.parseString(msg);
82 if (!message.has("CommuniqueType")) {
83 logger.debug("No CommuniqueType found in message: {}", msg);
87 String communiqueType = message.get("CommuniqueType").getAsString();
88 // CommuniqueType type = CommuniqueType.valueOf(communiqueType);
89 logger.debug("Received CommuniqueType: {}", communiqueType);
90 callback.validMessageReceived(communiqueType);
92 switch (communiqueType) {
93 case "CreateResponse":
96 handleReadResponseMessage(message);
98 case "UpdateResponse":
100 case "SubscribeResponse":
101 // Subscribe responses can contain bodies with data
102 handleReadResponseMessage(message);
104 case "UnsubscribeResponse":
106 case "ExceptionResponse":
107 handleExceptionResponse(message);
110 logger.debug("Unknown CommuniqueType received: {}", communiqueType);
113 } catch (JsonParseException e) {
114 logger.debug("Error parsing message: {}", e.getMessage());
120 * Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
122 * @param message LEAP message
124 private void handleExceptionResponse(JsonObject message) {
125 String detailMessage = "";
128 JsonObject header = message.get("Header").getAsJsonObject();
129 Header headerObj = gson.fromJson(header, Header.class);
131 if (MessageBodyType.ExceptionDetail.toString().equalsIgnoreCase(headerObj.messageBodyType)
132 && message.has("Body")) {
133 JsonObject body = message.get("Body").getAsJsonObject();
134 ExceptionDetail exceptionDetail = gson.fromJson(body, ExceptionDetail.class);
135 if (exceptionDetail != null) {
136 detailMessage = exceptionDetail.message;
139 logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
140 headerObj.url, detailMessage);
142 } catch (JsonParseException | IllegalStateException e) {
143 logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
149 * Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
151 * @param message LEAP message
153 private void handleReadResponseMessage(JsonObject message) {
155 JsonObject header = message.get("Header").getAsJsonObject();
156 Header headerObj = gson.fromJson(header, Header.class);
158 // if 204/NoContent response received for buttongroup request, create empty button map
159 if (Request.BUTTON_GROUP_URL.equals(headerObj.url)
160 && Header.STATUS_NO_CONTENT.equalsIgnoreCase(headerObj.statusCode)) {
161 callback.handleEmptyButtonGroupDefinition();
165 if (!header.has("MessageBodyType")) {
166 logger.trace("No MessageBodyType in header");
169 String messageBodyType = header.get("MessageBodyType").getAsString();
170 logger.trace("MessageBodyType: {}", messageBodyType);
172 if (!message.has("Body")) {
173 logger.debug("No Body found in message");
176 JsonObject body = message.get("Body").getAsJsonObject();
178 switch (messageBodyType) {
179 case "OnePingResponse":
180 parseOnePingResponse(body);
182 case "OneZoneStatus":
183 parseOneZoneStatus(body);
185 case "OneProjectDefinition":
186 parseOneProjectDefinition(body);
188 case "OneDeviceDefinition":
189 parseOneDeviceDefinition(body);
191 case "MultipleAreaDefinition":
192 parseMultipleAreaDefinition(body);
194 case "MultipleButtonGroupDefinition":
195 parseMultipleButtonGroupDefinition(body);
197 case "MultipleDeviceDefinition":
198 parseMultipleDeviceDefinition(body);
200 case "MultipleOccupancyGroupDefinition":
201 parseMultipleOccupancyGroupDefinition(body);
203 case "MultipleOccupancyGroupStatus":
204 parseMultipleOccupancyGroupStatus(body);
206 case "MultipleVirtualButtonDefinition":
208 case "MultipleZoneStatus":
209 parseMultipleZoneStatus(body);
212 logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
215 } catch (JsonParseException | IllegalStateException e) {
216 logger.debug("Error parsing message: {}", e.getMessage());
221 private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
224 if (messageBody.has(memberName)) {
225 JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
227 T obj = gson.fromJson(jsonObject, type);
230 logger.debug("Member name {} not found in JSON message", memberName);
233 } catch (IllegalStateException | JsonSyntaxException e) {
234 logger.debug("Error parsing JSON message: {}", e.getMessage());
239 private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
241 List<T> objList = new LinkedList<>();
243 if (messageBody.has(memberName)) {
244 JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
246 for (JsonElement element : jsonArray) {
247 JsonObject jsonObject = element.getAsJsonObject();
248 T obj = Objects.requireNonNull(gson.fromJson(jsonObject, type));
253 logger.debug("Member name {} not found in JSON message", memberName);
256 } catch (IllegalStateException | JsonSyntaxException e) {
257 logger.debug("Error parsing JSON message: {}", e.getMessage());
262 private void parseOnePingResponse(JsonObject messageBody) {
263 logger.debug("Ping response received");
267 * Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
269 private void parseOneZoneStatus(JsonObject messageBody) {
270 ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
271 if (zoneStatus != null) {
272 callback.handleZoneUpdate(zoneStatus);
277 * Parses a MultipleAreaDefinition message body.
279 private void parseMultipleAreaDefinition(JsonObject messageBody) {
280 logger.trace("Parsing area list");
281 List<Area> areaList = parseBodyMultiple(messageBody, "Areas", Area.class);
282 callback.handleMultipleAreaDefinition(areaList);
286 * Parses a MultipleOccupancyGroupDefinition message body.
288 private void parseMultipleOccupancyGroupDefinition(JsonObject messageBody) {
289 logger.trace("Parsing occupancy group list");
290 List<OccupancyGroup> oGroupList = parseBodyMultiple(messageBody, "OccupancyGroups", OccupancyGroup.class);
291 callback.handleMultipleOccupancyGroupDefinition(oGroupList);
295 * Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
297 private void parseMultipleOccupancyGroupStatus(JsonObject messageBody) {
298 logger.trace("Parsing occupancy group status list");
299 List<OccupancyGroupStatus> statusList = parseBodyMultiple(messageBody, "OccupancyGroupStatuses",
300 OccupancyGroupStatus.class);
301 for (OccupancyGroupStatus status : statusList) {
302 int groupNumber = status.getOccupancyGroup();
303 if (groupNumber > 0) {
304 logger.debug("OccupancyGroup: {} Status: {}", groupNumber, status.occupancyStatus);
305 callback.handleGroupUpdate(groupNumber, status.occupancyStatus);
310 private void parseMultipleZoneStatus(JsonObject messageBody) {
311 List<ZoneStatus> statusList = parseBodyMultiple(messageBody, "ZoneStatuses", ZoneStatus.class);
312 for (ZoneStatus status : statusList) {
313 logger.debug("Setting zone {} to level: {}", status.href, status.level);
314 callback.handleZoneUpdate(status);
319 * Parses a MultipleDeviceDefinition message body and loads the zoneToDevice and deviceToZone maps. Also passes the
320 * device data on to the discovery service and calls setBridgeProperties() with the hub's device entry.
322 private void parseMultipleDeviceDefinition(JsonObject messageBody) {
323 List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
324 callback.handleMultipleDeviceDefinition(deviceList);
328 * Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
330 private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
331 List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
332 callback.handleMultipleButtonGroupDefinition(buttonGroupList);
335 private void parseOneProjectDefinition(JsonObject messageBody) {
336 Project project = parseBodySingle(messageBody, "Project", Project.class);
337 if (project != null) {
338 callback.handleProjectDefinition(project);
342 private void parseOneDeviceDefinition(JsonObject messageBody) {
343 Device device = parseBodySingle(messageBody, "Device", Device.class);
344 if (device != null) {
345 callback.handleDeviceDefinition(device);