]> git.basschouten.com Git - openhab-addons.git/blob
4429b4b7b29d8892d69e9b847b280cef70804ccd
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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 import java.util.Objects;
18
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;
32
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;
41
42 /**
43  * Class responsible for parsing incoming LEAP messages. Calls back to an object implementing the
44  * LeapMessageParserCallbacks interface.
45  *
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.
48  *
49  * @author Bob Adair - Initial contribution
50  */
51 @NonNullByDefault
52 public class LeapMessageParser {
53     private final Logger logger = LoggerFactory.getLogger(LeapMessageParser.class);
54
55     private final Gson gson;
56     private final LeapMessageParserCallbacks callback;
57
58     /**
59      * LeapMessageParser Constructor
60      *
61      * @param callback Object implementing the LeapMessageParserCallbacks interface
62      */
63     public LeapMessageParser(LeapMessageParserCallbacks callback) {
64         gson = new GsonBuilder().create();
65         this.callback = callback;
66     }
67
68     /**
69      * Parse and process a LEAP protocol message
70      *
71      * @param msg String containing the LEAP message
72      */
73     public void handleMessage(String msg) {
74         if ("".equals(msg.trim())) {
75             return; // Ignore empty lines
76         }
77         logger.trace("Received message: {}", msg);
78
79         try {
80             JsonObject message = (JsonObject) JsonParser.parseString(msg);
81
82             if (!message.has("CommuniqueType")) {
83                 logger.debug("No CommuniqueType found in message: {}", msg);
84                 return;
85             }
86
87             String communiqueType = message.get("CommuniqueType").getAsString();
88             // CommuniqueType type = CommuniqueType.valueOf(communiqueType);
89             logger.debug("Received CommuniqueType: {}", communiqueType);
90             callback.validMessageReceived(communiqueType);
91
92             switch (communiqueType) {
93                 case "CreateResponse":
94                     return;
95                 case "ReadResponse":
96                     handleReadResponseMessage(message);
97                     break;
98                 case "UpdateResponse":
99                     break;
100                 case "SubscribeResponse":
101                     // Subscribe responses can contain bodies with data
102                     handleReadResponseMessage(message);
103                     return;
104                 case "UnsubscribeResponse":
105                     return;
106                 case "ExceptionResponse":
107                     handleExceptionResponse(message);
108                     return;
109                 default:
110                     logger.debug("Unknown CommuniqueType received: {}", communiqueType);
111                     break;
112             }
113         } catch (JsonParseException e) {
114             logger.debug("Error parsing message: {}", e.getMessage());
115             return;
116         }
117     }
118
119     /**
120      * Method called by handleMessage() to handle all LEAP ExceptionResponse messages.
121      *
122      * @param message LEAP message
123      */
124     private void handleExceptionResponse(JsonObject message) {
125         String detailMessage = "";
126
127         try {
128             JsonObject header = message.get("Header").getAsJsonObject();
129             Header headerObj = gson.fromJson(header, Header.class);
130
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;
137                 }
138             }
139             logger.debug("Exception response received. Status: {} URL: {} Message: {}", headerObj.statusCode,
140                     headerObj.url, detailMessage);
141
142         } catch (JsonParseException | IllegalStateException e) {
143             logger.debug("Exception response received. Error parsing exception message: {}", e.getMessage());
144             return;
145         }
146     }
147
148     /**
149      * Method called by handleMessage() to handle all LEAP ReadResponse and SubscribeResponse messages.
150      *
151      * @param message LEAP message
152      */
153     private void handleReadResponseMessage(JsonObject message) {
154         try {
155             JsonObject header = message.get("Header").getAsJsonObject();
156             Header headerObj = gson.fromJson(header, Header.class);
157
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();
162                 return;
163             }
164
165             if (!header.has("MessageBodyType")) {
166                 logger.trace("No MessageBodyType in header");
167                 return;
168             }
169             String messageBodyType = header.get("MessageBodyType").getAsString();
170             logger.trace("MessageBodyType: {}", messageBodyType);
171
172             if (!message.has("Body")) {
173                 logger.debug("No Body found in message");
174                 return;
175             }
176             JsonObject body = message.get("Body").getAsJsonObject();
177
178             switch (messageBodyType) {
179                 case "OnePingResponse":
180                     parseOnePingResponse(body);
181                     break;
182                 case "OneZoneStatus":
183                     parseOneZoneStatus(body);
184                     break;
185                 case "OneProjectDefinition":
186                     parseOneProjectDefinition(body);
187                     break;
188                 case "OneDeviceDefinition":
189                     parseOneDeviceDefinition(body);
190                     break;
191                 case "MultipleAreaDefinition":
192                     parseMultipleAreaDefinition(body);
193                     break;
194                 case "MultipleButtonGroupDefinition":
195                     parseMultipleButtonGroupDefinition(body);
196                     break;
197                 case "MultipleDeviceDefinition":
198                     parseMultipleDeviceDefinition(body);
199                     break;
200                 case "MultipleOccupancyGroupDefinition":
201                     parseMultipleOccupancyGroupDefinition(body);
202                     break;
203                 case "MultipleOccupancyGroupStatus":
204                     parseMultipleOccupancyGroupStatus(body);
205                     break;
206                 case "MultipleVirtualButtonDefinition":
207                     break;
208                 case "MultipleZoneStatus":
209                     parseMultipleZoneStatus(body);
210                     break;
211                 default:
212                     logger.debug("Unknown MessageBodyType received: {}", messageBodyType);
213                     break;
214             }
215         } catch (JsonParseException | IllegalStateException e) {
216             logger.debug("Error parsing message: {}", e.getMessage());
217             return;
218         }
219     }
220
221     private @Nullable <T extends AbstractMessageBody> T parseBodySingle(JsonObject messageBody, String memberName,
222             Class<T> type) {
223         try {
224             if (messageBody.has(memberName)) {
225                 JsonObject jsonObject = messageBody.get(memberName).getAsJsonObject();
226                 @Nullable
227                 T obj = gson.fromJson(jsonObject, type);
228                 return obj;
229             } else {
230                 logger.debug("Member name {} not found in JSON message", memberName);
231                 return null;
232             }
233         } catch (IllegalStateException | JsonSyntaxException e) {
234             logger.debug("Error parsing JSON message: {}", e.getMessage());
235             return null;
236         }
237     }
238
239     private <T extends AbstractMessageBody> List<T> parseBodyMultiple(JsonObject messageBody, String memberName,
240             Class<T> type) {
241         List<T> objList = new LinkedList<T>();
242         try {
243             if (messageBody.has(memberName)) {
244                 JsonArray jsonArray = messageBody.get(memberName).getAsJsonArray();
245
246                 for (JsonElement element : jsonArray) {
247                     JsonObject jsonObject = element.getAsJsonObject();
248                     T obj = Objects.requireNonNull(gson.fromJson(jsonObject, type));
249                     objList.add(obj);
250                 }
251                 return objList;
252             } else {
253                 logger.debug("Member name {} not found in JSON message", memberName);
254                 return objList;
255             }
256         } catch (IllegalStateException | JsonSyntaxException e) {
257             logger.debug("Error parsing JSON message: {}", e.getMessage());
258             return objList;
259         }
260     }
261
262     private void parseOnePingResponse(JsonObject messageBody) {
263         logger.debug("Ping response received");
264     }
265
266     /**
267      * Parses a OneZoneStatus message body. Calls handleZoneUpdate() to dispatch zone updates.
268      */
269     private void parseOneZoneStatus(JsonObject messageBody) {
270         ZoneStatus zoneStatus = parseBodySingle(messageBody, "ZoneStatus", ZoneStatus.class);
271         if (zoneStatus != null) {
272             callback.handleZoneUpdate(zoneStatus);
273         }
274     }
275
276     /**
277      * Parses a MultipleAreaDefinition message body.
278      */
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);
283     }
284
285     /**
286      * Parses a MultipleOccupancyGroupDefinition message body.
287      */
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);
292     }
293
294     /**
295      * Parses a MultipleOccupancyGroupStatus message body and updates occupancy status.
296      */
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);
306             }
307         }
308     }
309
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);
315         }
316     }
317
318     /**
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.
321      */
322     private void parseMultipleDeviceDefinition(JsonObject messageBody) {
323         List<Device> deviceList = parseBodyMultiple(messageBody, "Devices", Device.class);
324         callback.handleMultipleDeviceDefinition(deviceList);
325     }
326
327     /**
328      * Parse a MultipleButtonGroupDefinition message body and load the results into deviceButtonMap.
329      */
330     private void parseMultipleButtonGroupDefinition(JsonObject messageBody) {
331         List<ButtonGroup> buttonGroupList = parseBodyMultiple(messageBody, "ButtonGroups", ButtonGroup.class);
332         callback.handleMultipleButtonGroupDefinition(buttonGroupList);
333     }
334
335     private void parseOneProjectDefinition(JsonObject messageBody) {
336         Project project = parseBodySingle(messageBody, "Project", Project.class);
337         if (project != null) {
338             callback.handleProjectDefinition(project);
339         }
340     }
341
342     private void parseOneDeviceDefinition(JsonObject messageBody) {
343         Device device = parseBodySingle(messageBody, "Device", Device.class);
344         if (device != null) {
345             callback.handleDeviceDefinition(device);
346         }
347     }
348 }