2 * Copyright (c) 2010-2020 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;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.lutron.internal.protocol.leap.dto.Area;
21 import org.openhab.binding.lutron.internal.protocol.leap.dto.ButtonGroup;
22 import org.openhab.binding.lutron.internal.protocol.leap.dto.Device;
23 import org.openhab.binding.lutron.internal.protocol.leap.dto.ExceptionDetail;
24 import org.openhab.binding.lutron.internal.protocol.leap.dto.Header;
25 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroup;
26 import org.openhab.binding.lutron.internal.protocol.leap.dto.OccupancyGroupStatus;
27 import org.openhab.binding.lutron.internal.protocol.leap.dto.ZoneStatus;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
31 import com.google.gson.Gson;
32 import com.google.gson.GsonBuilder;
33 import com.google.gson.JsonArray;
34 import com.google.gson.JsonElement;
35 import com.google.gson.JsonObject;
36 import com.google.gson.JsonParseException;
37 import com.google.gson.JsonParser;
38 import com.google.gson.JsonSyntaxException;
41 * Class responsible for parsing incoming LEAP messages. Calls back to an object implementing the
42 * LeapMessageParserCallbacks interface.
44 * Thanks to the authors of the pylutron-caseta Python API (github.com/gurumitts/pylutron-caseta), which I used as a
45 * reference when first researching the LEAP protocol.
47 * @author Bob Adair - Initial contribution
50 public class LeapMessageParser {
51 private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
53 private final Gson gson;
54 private final LeapMessageParserCallbacks callback;
57 * LeapMessageParser Constructor
59 * @param callback Object implementing the LeapMessageParserCallbacks interface
61 public LeapMessageParser(LeapMessageParserCallbacks callback) {
62 gson = new GsonBuilder().create();
63 this.callback = callback;
67 * Parse and process a LEAP protocol message
69 * @param msg String containing the LEAP message
71 public void handleMessage(String msg) {
72 if (msg.trim().equals("")) {
73 return; // Ignore empty lines
75 logger.trace("Received message: {}", msg);
78 JsonObject message = (JsonObject) new JsonParser().parse(msg);
80 if (!message.has("CommuniqueType")) {
81 logger.debug("No CommuniqueType found in message: {}", msg);
85 String communiqueType = message.get("CommuniqueType").getAsString();
86 // CommuniqueType type = CommuniqueType.valueOf(communiqueType);
87 logger.debug("Received CommuniqueType: {}", communiqueType);
88 callback.validMessageReceived(communiqueType);
90 switch (communiqueType) {
91 case "CreateResponse":
94 handleReadResponseMessage(message);
96 case "UpdateResponse":
98 case "SubscribeResponse":
99 // Subscribe responses can contain bodies with data
100 handleReadResponseMessage(message);
102 case "UnsubscribeResponse":
104 case "ExceptionResponse":
105 handleExceptionResponse(message);
108 logger.debug("Unknown CommuniqueType received: {}", communiqueType);
111 } catch (JsonParseException e) {
112 logger.debug("Error parsing message: {}", e.getMessage());
118 * Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
120 * @param message LEAP message
122 private void handleExceptionResponse(JsonObject message) {
123 String detailMessage = "";
126 JsonObject header = message.get("Header").getAsJsonObject();
127 Header headerObj = gson.fromJson(header, Header.class);
129 if (MessageBodyType.ExceptionDetail.toString().equalsIgnoreCase(headerObj.messageBodyType)
130 && message.has("Body")) {
131 JsonObject body = message.get("Body").getAsJsonObject();
132 ExceptionDetail exceptionDetail = gson.fromJson(body, ExceptionDetail.class);
133 if (exceptionDetail != null) {
134 detailMessage = exceptionDetail.message;
137 logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
138 headerObj.url, detailMessage);
140 } catch (JsonParseException | IllegalStateException e) {
141 logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
147 * Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
149 * @param message LEAP message
151 private void handleReadResponseMessage(JsonObject message) {
153 JsonObject header = message.get("Header").getAsJsonObject();
154 Header headerObj = gson.fromJson(header, Header.class);
156 // if 204/NoContent response received for buttongroup request, create empty button map
157 if (Request.BUTTON_GROUP_URL.equals(headerObj.url)
158 && Header.STATUS_NO_CONTENT.equalsIgnoreCase(headerObj.statusCode)) {
159 callback.handleEmptyButtonGroupDefinition();
163 if (!header.has("MessageBodyType")) {
164 logger.trace("No MessageBodyType in header");
167 String messageBodyType = header.get("MessageBodyType").getAsString();
168 logger.trace("MessageBodyType: {}", messageBodyType);
170 if (!message.has("Body")) {
171 logger.debug("No Body found in message");
174 JsonObject body = message.get("Body").getAsJsonObject();
176 switch (messageBodyType) {
177 case "OnePingResponse":
178 parseOnePingResponse(body);
180 case "OneZoneStatus":
181 parseOneZoneStatus(body);
183 case "MultipleAreaDefinition":
184 parseMultipleAreaDefinition(body);
186 case "MultipleButtonGroupDefinition":
187 parseMultipleButtonGroupDefinition(body);
189 case "MultipleDeviceDefinition":
190 parseMultipleDeviceDefinition(body);
192 case "MultipleOccupancyGroupDefinition":
193 parseMultipleOccupancyGroupDefinition(body);
195 case "MultipleOccupancyGroupStatus":
196 parseMultipleOccupancyGroupStatus(body);
198 case "MultipleVirtualButtonDefinition":
201 logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
204 } catch (JsonParseException | IllegalStateException e) {
205 logger.debug("Error parsing message: {}", e.getMessage());
210 private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
213 if (messageBody.has(memberName)) {
214 JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
215 T obj = gson.fromJson(jsonObject, type);
218 logger.debug("Member name {} not found in JSON message", memberName);
221 } catch (IllegalStateException | JsonSyntaxException e) {
222 logger.debug("Error parsing JSON message: {}", e.getMessage());
227 private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
229 List<T> objList = new LinkedList<T>();
231 if (messageBody.has(memberName)) {
232 JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
234 for (JsonElement element : jsonArray) {
235 JsonObject jsonObject = element.getAsJsonObject();
236 T obj = gson.fromJson(jsonObject, type);
241 logger.debug("Member name {} not found in JSON message", memberName);
244 } catch (IllegalStateException | JsonSyntaxException e) {
245 logger.debug("Error parsing JSON message: {}", e.getMessage());
250 private void parseOnePingResponse(JsonObject messageBody) {
251 logger.debug("Ping response received");
255 * Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
257 private void parseOneZoneStatus(JsonObject messageBody) {
258 ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
259 if (zoneStatus != null) {
260 callback.handleZoneUpdate(zoneStatus);
265 * Parses a MultipleAreaDefinition message body.
267 private void parseMultipleAreaDefinition(JsonObject messageBody) {
268 logger.trace("Parsing area list");
269 List<Area> areaList = parseBodyMultiple(messageBody, "Areas", Area.class);
270 callback.handleMultipleAreaDefinition(areaList);
274 * Parses a MultipleOccupancyGroupDefinition message body.
276 private void parseMultipleOccupancyGroupDefinition(JsonObject messageBody) {
277 logger.trace("Parsing occupancy group list");
278 List<OccupancyGroup> oGroupList = parseBodyMultiple(messageBody, "OccupancyGroups", OccupancyGroup.class);
279 callback.handleMultipleOccupancyGroupDefinition(oGroupList);
283 * Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
285 private void parseMultipleOccupancyGroupStatus(JsonObject messageBody) {
286 logger.trace("Parsing occupancy group status list");
287 List<OccupancyGroupStatus> statusList = parseBodyMultiple(messageBody, "OccupancyGroupStatuses",
288 OccupancyGroupStatus.class);
289 for (OccupancyGroupStatus status : statusList) {
290 int groupNumber = status.getOccupancyGroup();
291 if (groupNumber > 0) {
292 logger.debug("OccupancyGroup: {} Status: {}", groupNumber, status.occupancyStatus);
293 callback.handleGroupUpdate(groupNumber, status.occupancyStatus);
299 * Parses a MultipleDeviceDefinition message body and loads the zoneToDevice and deviceToZone maps. Also passes the
300 * device data on to the discovery service and calls setBridgeProperties() with the hub's device entry.
302 private void parseMultipleDeviceDefinition(JsonObject messageBody) {
303 List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
304 callback.handleMultipleDeviceDefintion(deviceList);
308 * Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
310 private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
311 List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
312 callback.handleMultipleButtonGroupDefinition(buttonGroupList);