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