2 * Copyright (c) 2010-2022 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.ZoneStatus;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import com.google.gson.Gson;
33 import com.google.gson.GsonBuilder;
34 import com.google.gson.JsonArray;
35 import com.google.gson.JsonElement;
36 import com.google.gson.JsonObject;
37 import com.google.gson.JsonParseException;
38 import com.google.gson.JsonParser;
39 import com.google.gson.JsonSyntaxException;
42 * Class responsible for parsing incoming LEAP messages. Calls back to an object implementing the
43 * LeapMessageParserCallbacks interface.
45 * Thanks to the authors of the pylutron-caseta Python API (github.com/gurumitts/pylutron-caseta), which I used as a
46 * reference when first researching the LEAP protocol.
48 * @author Bob Adair - Initial contribution
51 public class LeapMessageParser {
52 private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
54 private final Gson gson;
55 private final LeapMessageParserCallbacks callback;
58 * LeapMessageParser Constructor
60 * @param callback Object implementing the LeapMessageParserCallbacks interface
62 public LeapMessageParser(LeapMessageParserCallbacks callback) {
63 gson = new GsonBuilder().create();
64 this.callback = callback;
68 * Parse and process a LEAP protocol message
70 * @param msg String containing the LEAP message
72 public void handleMessage(String msg) {
73 if (msg.trim().equals("")) {
74 return; // Ignore empty lines
76 logger.trace("Received message: {}", msg);
79 JsonObject message = (JsonObject) JsonParser.parseString(msg);
81 if (!message.has("CommuniqueType")) {
82 logger.debug("No CommuniqueType found in message: {}", msg);
86 String communiqueType = message.get("CommuniqueType").getAsString();
87 // CommuniqueType type = CommuniqueType.valueOf(communiqueType);
88 logger.debug("Received CommuniqueType: {}", communiqueType);
89 callback.validMessageReceived(communiqueType);
91 switch (communiqueType) {
92 case "CreateResponse":
95 handleReadResponseMessage(message);
97 case "UpdateResponse":
99 case "SubscribeResponse":
100 // Subscribe responses can contain bodies with data
101 handleReadResponseMessage(message);
103 case "UnsubscribeResponse":
105 case "ExceptionResponse":
106 handleExceptionResponse(message);
109 logger.debug("Unknown CommuniqueType received: {}", communiqueType);
112 } catch (JsonParseException e) {
113 logger.debug("Error parsing message: {}", e.getMessage());
119 * Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
121 * @param message LEAP message
123 private void handleExceptionResponse(JsonObject message) {
124 String detailMessage = "";
127 JsonObject header = message.get("Header").getAsJsonObject();
128 Header headerObj = gson.fromJson(header, Header.class);
130 if (MessageBodyType.ExceptionDetail.toString().equalsIgnoreCase(headerObj.messageBodyType)
131 && message.has("Body")) {
132 JsonObject body = message.get("Body").getAsJsonObject();
133 ExceptionDetail exceptionDetail = gson.fromJson(body, ExceptionDetail.class);
134 if (exceptionDetail != null) {
135 detailMessage = exceptionDetail.message;
138 logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
139 headerObj.url, detailMessage);
141 } catch (JsonParseException | IllegalStateException e) {
142 logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
148 * Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
150 * @param message LEAP message
152 private void handleReadResponseMessage(JsonObject message) {
154 JsonObject header = message.get("Header").getAsJsonObject();
155 Header headerObj = gson.fromJson(header, Header.class);
157 // if 204/NoContent response received for buttongroup request, create empty button map
158 if (Request.BUTTON_GROUP_URL.equals(headerObj.url)
159 && Header.STATUS_NO_CONTENT.equalsIgnoreCase(headerObj.statusCode)) {
160 callback.handleEmptyButtonGroupDefinition();
164 if (!header.has("MessageBodyType")) {
165 logger.trace("No MessageBodyType in header");
168 String messageBodyType = header.get("MessageBodyType").getAsString();
169 logger.trace("MessageBodyType: {}", messageBodyType);
171 if (!message.has("Body")) {
172 logger.debug("No Body found in message");
175 JsonObject body = message.get("Body").getAsJsonObject();
177 switch (messageBodyType) {
178 case "OnePingResponse":
179 parseOnePingResponse(body);
181 case "OneZoneStatus":
182 parseOneZoneStatus(body);
184 case "MultipleAreaDefinition":
185 parseMultipleAreaDefinition(body);
187 case "MultipleButtonGroupDefinition":
188 parseMultipleButtonGroupDefinition(body);
190 case "MultipleDeviceDefinition":
191 parseMultipleDeviceDefinition(body);
193 case "MultipleOccupancyGroupDefinition":
194 parseMultipleOccupancyGroupDefinition(body);
196 case "MultipleOccupancyGroupStatus":
197 parseMultipleOccupancyGroupStatus(body);
199 case "MultipleVirtualButtonDefinition":
202 logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
205 } catch (JsonParseException | IllegalStateException e) {
206 logger.debug("Error parsing message: {}", e.getMessage());
211 private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
214 if (messageBody.has(memberName)) {
215 JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
217 T obj = gson.fromJson(jsonObject, type);
220 logger.debug("Member name {} not found in JSON message", memberName);
223 } catch (IllegalStateException | JsonSyntaxException e) {
224 logger.debug("Error parsing JSON message: {}", e.getMessage());
229 private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
231 List<T> objList = new LinkedList<T>();
233 if (messageBody.has(memberName)) {
234 JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
236 for (JsonElement element : jsonArray) {
237 JsonObject jsonObject = element.getAsJsonObject();
238 T obj = Objects.requireNonNull(gson.fromJson(jsonObject, type));
243 logger.debug("Member name {} not found in JSON message", memberName);
246 } catch (IllegalStateException | JsonSyntaxException e) {
247 logger.debug("Error parsing JSON message: {}", e.getMessage());
252 private void parseOnePingResponse(JsonObject messageBody) {
253 logger.debug("Ping response received");
257 * Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
259 private void parseOneZoneStatus(JsonObject messageBody) {
260 ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
261 if (zoneStatus != null) {
262 callback.handleZoneUpdate(zoneStatus);
267 * Parses a MultipleAreaDefinition message body.
269 private void parseMultipleAreaDefinition(JsonObject messageBody) {
270 logger.trace("Parsing area list");
271 List<Area> areaList = parseBodyMultiple(messageBody, "Areas", Area.class);
272 callback.handleMultipleAreaDefinition(areaList);
276 * Parses a MultipleOccupancyGroupDefinition message body.
278 private void parseMultipleOccupancyGroupDefinition(JsonObject messageBody) {
279 logger.trace("Parsing occupancy group list");
280 List<OccupancyGroup> oGroupList = parseBodyMultiple(messageBody, "OccupancyGroups", OccupancyGroup.class);
281 callback.handleMultipleOccupancyGroupDefinition(oGroupList);
285 * Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
287 private void parseMultipleOccupancyGroupStatus(JsonObject messageBody) {
288 logger.trace("Parsing occupancy group status list");
289 List<OccupancyGroupStatus> statusList = parseBodyMultiple(messageBody, "OccupancyGroupStatuses",
290 OccupancyGroupStatus.class);
291 for (OccupancyGroupStatus status : statusList) {
292 int groupNumber = status.getOccupancyGroup();
293 if (groupNumber > 0) {
294 logger.debug("OccupancyGroup: {} Status: {}", groupNumber, status.occupancyStatus);
295 callback.handleGroupUpdate(groupNumber, status.occupancyStatus);
301 * Parses a MultipleDeviceDefinition message body and loads the zoneToDevice and deviceToZone maps. Also passes the
302 * device data on to the discovery service and calls setBridgeProperties() with the hub's device entry.
304 private void parseMultipleDeviceDefinition(JsonObject messageBody) {
305 List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
306 callback.handleMultipleDeviceDefintion(deviceList);
310 * Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
312 private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
313 List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
314 callback.handleMultipleButtonGroupDefinition(buttonGroupList);