]> git.basschouten.com Git - openhab-addons.git/blob
0dff1cb7c59bb4cd1c794d0e76b2a2d5161bc1fd
[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.remoteopenhab.internal.rest;
14
15 import java.io.ByteArrayInputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.nio.charset.StandardCharsets;
19 import java.util.Arrays;
20 import java.util.List;
21 import java.util.Objects;
22 import java.util.Properties;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.TimeUnit;
25
26 import javax.ws.rs.client.Client;
27 import javax.ws.rs.client.ClientBuilder;
28 import javax.ws.rs.core.HttpHeaders;
29 import javax.ws.rs.sse.InboundSseEvent;
30 import javax.ws.rs.sse.SseEventSource;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.openhab.binding.remoteopenhab.internal.data.Event;
35 import org.openhab.binding.remoteopenhab.internal.data.EventPayload;
36 import org.openhab.binding.remoteopenhab.internal.data.Item;
37 import org.openhab.binding.remoteopenhab.internal.data.RestApi;
38 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
39 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
40 import org.openhab.core.io.net.http.HttpUtil;
41 import org.openhab.core.types.Command;
42 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import com.google.gson.Gson;
47 import com.google.gson.JsonSyntaxException;
48
49 /**
50  * A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
51  * Events (SSE).
52  *
53  * @author Laurent Garnier - Initial contribution
54  */
55 @NonNullByDefault
56 public class RemoteopenhabRestClient {
57
58     private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
59
60     private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
61
62     private final ClientBuilder clientBuilder;
63     private final SseEventSourceFactory eventSourceFactory;
64     private final Gson jsonParser;
65     private String accessToken;
66     private final String restUrl;
67
68     private final Object startStopLock = new Object();
69     private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
70
71     private @Nullable String restApiVersion;
72     private @Nullable String restApiItems;
73     private @Nullable String restApiEvents;
74     private @Nullable String topicNamespace;
75     private boolean connected;
76
77     private @Nullable SseEventSource eventSource;
78     private long lastEventTimestamp;
79
80     public RemoteopenhabRestClient(final ClientBuilder clientBuilder, final SseEventSourceFactory eventSourceFactory,
81             final Gson jsonParser, final String accessToken, final String restUrl) {
82         this.clientBuilder = clientBuilder;
83         this.eventSourceFactory = eventSourceFactory;
84         this.jsonParser = jsonParser;
85         this.accessToken = accessToken;
86         this.restUrl = restUrl;
87     }
88
89     public void tryApi() throws RemoteopenhabException {
90         try {
91             Properties httpHeaders = new Properties();
92             if (!accessToken.isEmpty()) {
93                 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
94             }
95             httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
96             String jsonResponse = HttpUtil.executeUrl("GET", restUrl, httpHeaders, null, null, REQUEST_TIMEOUT);
97             if (jsonResponse.isEmpty()) {
98                 throw new RemoteopenhabException("Failed to execute the root REST API");
99             }
100             RestApi restApi = jsonParser.fromJson(jsonResponse, RestApi.class);
101             restApiVersion = restApi.version;
102             logger.debug("REST API version = {}", restApiVersion);
103             restApiItems = null;
104             for (int i = 0; i < restApi.links.length; i++) {
105                 if ("items".equals(restApi.links[i].type)) {
106                     restApiItems = restApi.links[i].url;
107                 } else if ("events".equals(restApi.links[i].type)) {
108                     restApiEvents = restApi.links[i].url;
109                 }
110             }
111             logger.debug("REST API items = {}", restApiItems);
112             logger.debug("REST API events = {}", restApiEvents);
113             topicNamespace = restApi.runtimeInfo != null ? "openhab" : "smarthome";
114             logger.debug("topic namespace = {}", topicNamespace);
115         } catch (RemoteopenhabException e) {
116             throw new RemoteopenhabException(e.getMessage());
117         } catch (JsonSyntaxException e) {
118             throw new RemoteopenhabException("Failed to parse the result of the root REST API", e);
119         } catch (IOException e) {
120             throw new RemoteopenhabException("Failed to execute the root REST API", e);
121         }
122     }
123
124     public List<Item> getRemoteItems() throws RemoteopenhabException {
125         try {
126             Properties httpHeaders = new Properties();
127             if (!accessToken.isEmpty()) {
128                 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
129             }
130             httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
131             String url = String.format("%s?recursive=fasle", getRestApiItems());
132             String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeaders, null, null, REQUEST_TIMEOUT);
133             return Arrays.asList(jsonParser.fromJson(jsonResponse, Item[].class));
134         } catch (IOException | JsonSyntaxException e) {
135             throw new RemoteopenhabException("Failed to get the list of remote items using the items REST API", e);
136         }
137     }
138
139     public String getRemoteItemState(String itemName) throws RemoteopenhabException {
140         try {
141             Properties httpHeaders = new Properties();
142             if (!accessToken.isEmpty()) {
143                 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
144             }
145             httpHeaders.put(HttpHeaders.ACCEPT, "text/plain");
146             String url = String.format("%s/%s/state", getRestApiItems(), itemName);
147             return HttpUtil.executeUrl("GET", url, httpHeaders, null, null, REQUEST_TIMEOUT);
148         } catch (IOException e) {
149             throw new RemoteopenhabException(
150                     "Failed to get the state of remote item " + itemName + " using the items REST API", e);
151         }
152     }
153
154     public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
155         try {
156             Properties httpHeaders = new Properties();
157             if (!accessToken.isEmpty()) {
158                 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
159             }
160             httpHeaders.put(HttpHeaders.ACCEPT, "application/json");
161             String url = String.format("%s/%s", getRestApiItems(), itemName);
162             InputStream stream = new ByteArrayInputStream(command.toFullString().getBytes(StandardCharsets.UTF_8));
163             HttpUtil.executeUrl("POST", url, httpHeaders, stream, "text/plain", REQUEST_TIMEOUT);
164             stream.close();
165         } catch (IOException e) {
166             throw new RemoteopenhabException(
167                     "Failed to send command to the remote item " + itemName + " using the items REST API", e);
168         }
169     }
170
171     public @Nullable String getRestApiVersion() {
172         return restApiVersion;
173     }
174
175     public String getRestApiItems() {
176         String url = restApiItems;
177         return url != null ? url : restUrl + "/items";
178     }
179
180     public String getRestApiEvents() {
181         String url = restApiEvents;
182         return url != null ? url : restUrl + "/events";
183     }
184
185     public String getTopicNamespace() {
186         String namespace = topicNamespace;
187         return namespace != null ? namespace : "openhab";
188     }
189
190     public void start() {
191         synchronized (startStopLock) {
192             logger.debug("Opening EventSource {}", getItemsRestSseUrl());
193             reopenEventSource();
194             logger.debug("EventSource started");
195         }
196     }
197
198     public void stop() {
199         synchronized (startStopLock) {
200             logger.debug("Closing EventSource {}", getItemsRestSseUrl());
201             closeEventSource(0, TimeUnit.SECONDS);
202             logger.debug("EventSource stopped");
203         }
204     }
205
206     private String getItemsRestSseUrl() {
207         return String.format("%s?topics=%s/items/*/*", getRestApiEvents(), getTopicNamespace());
208     }
209
210     private SseEventSource createEventSource(String restSseUrl) {
211         Client client = clientBuilder.register(new RemoteopenhabStreamingRequestFilter(accessToken)).build();
212         SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
213         eventSource.register(this::onEvent, this::onError);
214         return eventSource;
215     }
216
217     private void reopenEventSource() {
218         logger.debug("Reopening EventSource");
219         closeEventSource(10, TimeUnit.SECONDS);
220
221         logger.debug("Opening new EventSource {}", getItemsRestSseUrl());
222         SseEventSource localEventSource = createEventSource(getItemsRestSseUrl());
223         localEventSource.open();
224
225         eventSource = localEventSource;
226     }
227
228     private void closeEventSource(long timeout, TimeUnit timeoutUnit) {
229         SseEventSource localEventSource = eventSource;
230         if (localEventSource != null) {
231             if (!localEventSource.isOpen()) {
232                 logger.debug("Existing EventSource is already closed");
233             } else if (localEventSource.close(timeout, timeoutUnit)) {
234                 logger.debug("Succesfully closed existing EventSource");
235             } else {
236                 logger.debug("Failed to close existing EventSource");
237             }
238             eventSource = null;
239         }
240         connected = false;
241     }
242
243     public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
244         return listeners.add(listener);
245     }
246
247     public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
248         return listeners.remove(listener);
249     }
250
251     public long getLastEventTimestamp() {
252         return lastEventTimestamp;
253     }
254
255     private void onEvent(InboundSseEvent inboundEvent) {
256         String name = inboundEvent.getName();
257         String data = inboundEvent.readData();
258         logger.trace("Received event name {} date {}", name, data);
259
260         lastEventTimestamp = System.currentTimeMillis();
261         if (!connected) {
262             logger.debug("Connected to streaming events");
263             connected = true;
264             listeners.forEach(listener -> listener.onConnected());
265         }
266
267         if (!"message".equals(name)) {
268             logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
269             return;
270         }
271
272         try {
273             Event event = jsonParser.fromJson(data, Event.class);
274             String itemName;
275             EventPayload payload;
276             Item item;
277             switch (event.type) {
278                 case "ItemStateEvent":
279                     itemName = extractItemNameFromTopic(event.topic, event.type, "state");
280                     payload = jsonParser.fromJson(event.payload, EventPayload.class);
281                     listeners.forEach(listener -> listener.onItemStateEvent(itemName, payload.type, payload.value));
282                     break;
283                 case "GroupItemStateChangedEvent":
284                     itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
285                     payload = jsonParser.fromJson(event.payload, EventPayload.class);
286                     listeners.forEach(listener -> listener.onItemStateEvent(itemName, payload.type, payload.value));
287                     break;
288                 case "ItemAddedEvent":
289                     itemName = extractItemNameFromTopic(event.topic, event.type, "added");
290                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, Item.class));
291                     listeners.forEach(listener -> listener.onItemAdded(item));
292                     break;
293                 case "ItemRemovedEvent":
294                     itemName = extractItemNameFromTopic(event.topic, event.type, "removed");
295                     item = Objects.requireNonNull(jsonParser.fromJson(event.payload, Item.class));
296                     listeners.forEach(listener -> listener.onItemRemoved(item));
297                     break;
298                 case "ItemUpdatedEvent":
299                     itemName = extractItemNameFromTopic(event.topic, event.type, "updated");
300                     Item[] updItem = jsonParser.fromJson(event.payload, Item[].class);
301                     if (updItem.length == 2) {
302                         listeners.forEach(listener -> listener.onItemUpdated(updItem[0], updItem[1]));
303                     } else {
304                         logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
305                     }
306                     break;
307                 case "ItemStatePredictedEvent":
308                 case "ItemStateChangedEvent":
309                 case "ItemCommandEvent":
310                     logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
311                     break;
312                 default:
313                     logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
314                     break;
315             }
316         } catch (RemoteopenhabException | JsonSyntaxException e) {
317             logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
318                     e);
319         }
320     }
321
322     private void onError(Throwable error) {
323         logger.debug("Error occurred while receiving events", error);
324         listeners.forEach(listener -> listener.onError("Error occurred while receiving events"));
325     }
326
327     private String extractItemNameFromTopic(String topic, String eventType, String finalPart)
328             throws RemoteopenhabException {
329         String[] parts = topic.split("/");
330         int expectedNbParts = "GroupItemStateChangedEvent".equals(eventType) ? 5 : 4;
331         if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"items".equals(parts[1])
332                 || !finalPart.equals(parts[parts.length - 1])) {
333             throw new RemoteopenhabException("Invalid event topic " + topic + " for event type " + eventType);
334         }
335         return parts[2];
336     }
337 }