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