]> git.basschouten.com Git - openhab-addons.git/blob
a8d96050a1cf8dbcbebd2664b6a05acf63d74d29
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.lutron.internal.protocol.leap;
14
15 import java.util.LinkedList;
16 import java.util.List;
17
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;
30
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;
39
40 /**
41  * Class responsible for parsing incoming LEAP messages. Calls back to an object implementing the
42  * LeapMessageParserCallbacks interface.
43  *
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.
46  *
47  * @author Bob Adair - Initial contribution
48  */
49 @NonNullByDefault
50 public class LeapMessageParser {
51     private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
52
53     private final Gson gson;
54     private final LeapMessageParserCallbacks callback;
55
56     /**
57      * LeapMessageParser Constructor
58      *
59      * @param callback Object implementing the LeapMessageParserCallbacks interface
60      */
61     public LeapMessageParser(LeapMessageParserCallbacks callback) {
62         gson = new GsonBuilder().create();
63         this.callback = callback;
64     }
65
66     /**
67      * Parse and process a LEAP protocol message
68      *
69      * @param msg String containing the LEAP message
70      */
71     public void handleMessage(String msg) {
72         if (msg.trim().equals("")) {
73             return; // Ignore empty lines
74         }
75         logger.trace("Received message: {}", msg);
76
77         try {
78             JsonObject message = (JsonObject) new JsonParser().parse(msg);
79
80             if (!message.has("CommuniqueType")) {
81                 logger.debug("No CommuniqueType found in message: {}", msg);
82                 return;
83             }
84
85             String communiqueType = message.get("CommuniqueType").getAsString();
86             // CommuniqueType type = CommuniqueType.valueOf(communiqueType);
87             logger.debug("Received CommuniqueType: {}", communiqueType);
88             callback.validMessageReceived(communiqueType);
89
90             switch (communiqueType) {
91                 case "CreateResponse":
92                     return;
93                 case "ReadResponse":
94                     handleReadResponseMessage(message);
95                     break;
96                 case "UpdateResponse":
97                     break;
98                 case "SubscribeResponse":
99                     // Subscribe responses can contain bodies with data
100                     handleReadResponseMessage(message);
101                     return;
102                 case "UnsubscribeResponse":
103                     return;
104                 case "ExceptionResponse":
105                     handleExceptionResponse(message);
106                     return;
107                 default:
108                     logger.debug("Unknown CommuniqueType received: {}", communiqueType);
109                     break;
110             }
111         } catch (JsonParseException e) {
112             logger.debug("Error parsing message: {}", e.getMessage());
113             return;
114         }
115     }
116
117     /**
118      * Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
119      *
120      * @param message LEAP message
121      */
122     private void handleExceptionResponse(JsonObject message) {
123         String detailMessage = "";
124
125         try {
126             JsonObject header = message.get("Header").getAsJsonObject();
127             Header headerObj = gson.fromJson(header, Header.class);
128
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;
135                 }
136             }
137             logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
138                     headerObj.url, detailMessage);
139
140         } catch (JsonParseException | IllegalStateException e) {
141             logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
142             return;
143         }
144     }
145
146     /**
147      * Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
148      *
149      * @param message LEAP message
150      */
151     private void handleReadResponseMessage(JsonObject message) {
152         try {
153             JsonObject header = message.get("Header").getAsJsonObject();
154             Header headerObj = gson.fromJson(header, Header.class);
155
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();
160                 return;
161             }
162
163             if (!header.has("MessageBodyType")) {
164                 logger.trace("No MessageBodyType in header");
165                 return;
166             }
167             String messageBodyType = header.get("MessageBodyType").getAsString();
168             logger.trace("MessageBodyType: {}", messageBodyType);
169
170             if (!message.has("Body")) {
171                 logger.debug("No Body found in message");
172                 return;
173             }
174             JsonObject body = message.get("Body").getAsJsonObject();
175
176             switch (messageBodyType) {
177                 case "OnePingResponse":
178                     parseOnePingResponse(body);
179                     break;
180                 case "OneZoneStatus":
181                     parseOneZoneStatus(body);
182                     break;
183                 case "MultipleAreaDefinition":
184                     parseMultipleAreaDefinition(body);
185                     break;
186                 case "MultipleButtonGroupDefinition":
187                     parseMultipleButtonGroupDefinition(body);
188                     break;
189                 case "MultipleDeviceDefinition":
190                     parseMultipleDeviceDefinition(body);
191                     break;
192                 case "MultipleOccupancyGroupDefinition":
193                     parseMultipleOccupancyGroupDefinition(body);
194                     break;
195                 case "MultipleOccupancyGroupStatus":
196                     parseMultipleOccupancyGroupStatus(body);
197                     break;
198                 case "MultipleVirtualButtonDefinition":
199                     break;
200                 default:
201                     logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
202                     break;
203             }
204         } catch (JsonParseException | IllegalStateException e) {
205             logger.debug("Error parsing message: {}", e.getMessage());
206             return;
207         }
208     }
209
210     private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
211             Class<T> type) {
212         try {
213             if (messageBody.has(memberName)) {
214                 JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
215                 T obj = gson.fromJson(jsonObject, type);
216                 return obj;
217             } else {
218                 logger.debug("Member name {} not found in JSON message", memberName);
219                 return null;
220             }
221         } catch (IllegalStateException | JsonSyntaxException e) {
222             logger.debug("Error parsing JSON message: {}", e.getMessage());
223             return null;
224         }
225     }
226
227     private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
228             Class<T> type) {
229         List<T> objList = new LinkedList<T>();
230         try {
231             if (messageBody.has(memberName)) {
232                 JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
233
234                 for (JsonElement element : jsonArray) {
235                     JsonObject jsonObject = element.getAsJsonObject();
236                     T obj = gson.fromJson(jsonObject, type);
237                     objList.add(obj);
238                 }
239                 return objList;
240             } else {
241                 logger.debug("Member name {} not found in JSON message", memberName);
242                 return objList;
243             }
244         } catch (IllegalStateException | JsonSyntaxException e) {
245             logger.debug("Error parsing JSON message: {}", e.getMessage());
246             return objList;
247         }
248     }
249
250     private void parseOnePingResponse(JsonObject messageBody) {
251         logger.debug("Ping response received");
252     }
253
254     /**
255      * Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
256      */
257     private void parseOneZoneStatus(JsonObject messageBody) {
258         ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
259         if (zoneStatus != null) {
260             callback.handleZoneUpdate(zoneStatus);
261         }
262     }
263
264     /**
265      * Parses a MultipleAreaDefinition message body.
266      */
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);
271     }
272
273     /**
274      * Parses a MultipleOccupancyGroupDefinition message body.
275      */
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);
280     }
281
282     /**
283      * Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
284      */
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);
294             }
295         }
296     }
297
298     /**
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.
301      */
302     private void parseMultipleDeviceDefinition(JsonObject messageBody) {
303         List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
304         callback.handleMultipleDeviceDefintion(deviceList);
305     }
306
307     /**
308      * Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
309      */
310     private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
311         List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
312         callback.handleMultipleButtonGroupDefinition(buttonGroupList);
313     }
314 }