2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.remoteopenhab.internal.rest;
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;
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;
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;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonSyntaxException;
49 * A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
52 * @author Laurent Garnier - Initial contribution
55 public class RemoteopenhabRestClient {
57 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
59 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
61 private final ClientBuilder clientBuilder;
62 private final SseEventSourceFactory eventSourceFactory;
63 private final Gson jsonParser;
64 private String accessToken;
65 private final String restUrl;
67 private final Object startStopLock = new Object();
68 private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
70 private @Nullable String restApiVersion;
71 private @Nullable String restApiItems;
72 private @Nullable String restApiEvents;
73 private @Nullable String topicNamespace;
74 private boolean connected;
76 private @Nullable SseEventSource eventSource;
77 private long lastEventTimestamp;
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;
88 public void tryApi() throws RemoteopenhabException {
90 Properties httpHeaders = new Properties();
91 if (!accessToken.isEmpty()) {
92 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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");
99 RestApi restApi = jsonParser.fromJson(jsonResponse, RestApi.class);
100 restApiVersion = restApi.version;
101 logger.debug("REST API version = {}", restApiVersion);
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;
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);
123 public List<Item> getRemoteItems() throws RemoteopenhabException {
125 Properties httpHeaders = new Properties();
126 if (!accessToken.isEmpty()) {
127 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
138 public String getRemoteItemState(String itemName) throws RemoteopenhabException {
140 Properties httpHeaders = new Properties();
141 if (!accessToken.isEmpty()) {
142 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
153 public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
155 Properties httpHeaders = new Properties();
156 if (!accessToken.isEmpty()) {
157 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
164 } catch (IOException e) {
165 throw new RemoteopenhabException(
166 "Failed to send command to the remote item " + itemName + " using the items REST API", e);
170 public @Nullable String getRestApiVersion() {
171 return restApiVersion;
174 public String getRestApiItems() {
175 String url = restApiItems;
176 return url != null ? url : restUrl + "/items";
179 public String getRestApiEvents() {
180 String url = restApiEvents;
181 return url != null ? url : restUrl + "/events";
184 public String getTopicNamespace() {
185 String namespace = topicNamespace;
186 return namespace != null ? namespace : "openhab";
189 public void start() {
190 synchronized (startStopLock) {
191 logger.debug("Opening EventSource {}", getItemsRestSseUrl());
193 logger.debug("EventSource started");
198 synchronized (startStopLock) {
199 logger.debug("Closing EventSource {}", getItemsRestSseUrl());
200 closeEventSource(0, TimeUnit.SECONDS);
201 logger.debug("EventSource stopped");
205 private String getItemsRestSseUrl() {
206 return String.format("%s?topics=%s/items/*/*", getRestApiEvents(), getTopicNamespace());
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);
216 private void reopenEventSource() {
217 logger.debug("Reopening EventSource");
218 closeEventSource(10, TimeUnit.SECONDS);
220 logger.debug("Opening new EventSource {}", getItemsRestSseUrl());
221 SseEventSource localEventSource = createEventSource(getItemsRestSseUrl());
222 localEventSource.open();
224 eventSource = localEventSource;
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");
235 logger.debug("Failed to close existing EventSource");
242 public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
243 return listeners.add(listener);
246 public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
247 return listeners.remove(listener);
250 public long getLastEventTimestamp() {
251 return lastEventTimestamp;
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);
259 lastEventTimestamp = System.currentTimeMillis();
261 logger.debug("Connected to streaming events");
263 listeners.forEach(listener -> listener.onConnected());
266 if (!"message".equals(name)) {
267 logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
272 Event event = jsonParser.fromJson(data, Event.class);
274 EventPayload payload;
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));
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));
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));
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));
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]));
303 logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
306 case "ItemStatePredictedEvent":
307 case "ItemStateChangedEvent":
308 case "ItemCommandEvent":
309 logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
312 logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
315 } catch (RemoteopenhabException | JsonSyntaxException e) {
316 logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
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"));
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);