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.Objects;
22 import java.util.Properties;
23 import java.util.concurrent.CopyOnWriteArrayList;
24 import java.util.concurrent.TimeUnit;
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;
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;
46 import com.google.gson.Gson;
47 import com.google.gson.JsonSyntaxException;
50 * A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
53 * @author Laurent Garnier - Initial contribution
56 public class RemoteopenhabRestClient {
58 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
60 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
62 private final ClientBuilder clientBuilder;
63 private final SseEventSourceFactory eventSourceFactory;
64 private final Gson jsonParser;
65 private String accessToken;
66 private final String restUrl;
68 private final Object startStopLock = new Object();
69 private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
71 private @Nullable String restApiVersion;
72 private @Nullable String restApiItems;
73 private @Nullable String restApiEvents;
74 private @Nullable String topicNamespace;
75 private boolean connected;
77 private @Nullable SseEventSource eventSource;
78 private long lastEventTimestamp;
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;
89 public void tryApi() throws RemoteopenhabException {
91 Properties httpHeaders = new Properties();
92 if (!accessToken.isEmpty()) {
93 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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");
100 RestApi restApi = jsonParser.fromJson(jsonResponse, RestApi.class);
101 restApiVersion = restApi.version;
102 logger.debug("REST API version = {}", restApiVersion);
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;
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);
124 public List<Item> getRemoteItems() throws RemoteopenhabException {
126 Properties httpHeaders = new Properties();
127 if (!accessToken.isEmpty()) {
128 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
139 public String getRemoteItemState(String itemName) throws RemoteopenhabException {
141 Properties httpHeaders = new Properties();
142 if (!accessToken.isEmpty()) {
143 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
154 public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
156 Properties httpHeaders = new Properties();
157 if (!accessToken.isEmpty()) {
158 httpHeaders.put(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
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);
165 } catch (IOException e) {
166 throw new RemoteopenhabException(
167 "Failed to send command to the remote item " + itemName + " using the items REST API", e);
171 public @Nullable String getRestApiVersion() {
172 return restApiVersion;
175 public String getRestApiItems() {
176 String url = restApiItems;
177 return url != null ? url : restUrl + "/items";
180 public String getRestApiEvents() {
181 String url = restApiEvents;
182 return url != null ? url : restUrl + "/events";
185 public String getTopicNamespace() {
186 String namespace = topicNamespace;
187 return namespace != null ? namespace : "openhab";
190 public void start() {
191 synchronized (startStopLock) {
192 logger.debug("Opening EventSource {}", getItemsRestSseUrl());
194 logger.debug("EventSource started");
199 synchronized (startStopLock) {
200 logger.debug("Closing EventSource {}", getItemsRestSseUrl());
201 closeEventSource(0, TimeUnit.SECONDS);
202 logger.debug("EventSource stopped");
206 private String getItemsRestSseUrl() {
207 return String.format("%s?topics=%s/items/*/*", getRestApiEvents(), getTopicNamespace());
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);
217 private void reopenEventSource() {
218 logger.debug("Reopening EventSource");
219 closeEventSource(10, TimeUnit.SECONDS);
221 logger.debug("Opening new EventSource {}", getItemsRestSseUrl());
222 SseEventSource localEventSource = createEventSource(getItemsRestSseUrl());
223 localEventSource.open();
225 eventSource = localEventSource;
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");
236 logger.debug("Failed to close existing EventSource");
243 public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
244 return listeners.add(listener);
247 public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
248 return listeners.remove(listener);
251 public long getLastEventTimestamp() {
252 return lastEventTimestamp;
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);
260 lastEventTimestamp = System.currentTimeMillis();
262 logger.debug("Connected to streaming events");
264 listeners.forEach(listener -> listener.onConnected());
267 if (!"message".equals(name)) {
268 logger.debug("Received unhandled event with name '{}' and data '{}'", name, data);
273 Event event = jsonParser.fromJson(data, Event.class);
275 EventPayload payload;
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));
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));
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));
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));
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]));
304 logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
307 case "ItemStatePredictedEvent":
308 case "ItemStateChangedEvent":
309 case "ItemCommandEvent":
310 logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
313 logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
316 } catch (RemoteopenhabException | JsonSyntaxException e) {
317 logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
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"));
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);