]> git.basschouten.com Git - openhab-addons.git/blob
1846eeaa4b541350b637097343fde2794f3e73a9
[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.webthing.internal.client;
14
15 import java.io.IOException;
16 import java.net.URI;
17 import java.time.Duration;
18 import java.util.Map;
19 import java.util.Optional;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledExecutorService;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.concurrent.atomic.AtomicBoolean;
25 import java.util.function.BiConsumer;
26 import java.util.function.Consumer;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.websocket.client.WebSocketClient;
33 import org.openhab.binding.webthing.internal.client.dto.Property;
34 import org.openhab.binding.webthing.internal.client.dto.WebThingDescription;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import com.google.gson.Gson;
39
40 /**
41  * The implementation of the client-side Webthing representation. This is based on HTTP. Bindings to alternative
42  * application protocols such as CoAP may be defined in the future (which may be implemented by another class)
43  *
44  * @author Gregor Roth - Initial contribution
45  */
46 @NonNullByDefault
47 public class ConsumedThingImpl implements ConsumedThing {
48     private static final Duration DEFAULT_PING_PERIOD = Duration.ofSeconds(80);
49     private final Logger logger = LoggerFactory.getLogger(ConsumedThingImpl.class);
50     private final URI webThingURI;
51     private final Gson gson = new Gson();
52     private final HttpClient httpClient;
53     private final Consumer<String> errorHandler;
54     private final WebThingDescription description;
55     private final WebSocketConnection websocketDownstream;
56     private final AtomicBoolean isOpen = new AtomicBoolean(true);
57
58     /**
59      * constructor
60      *
61      * @param webSocketClient the web socket client to use
62      * @param httpClient the http client to use
63      * @param webThingURI the identifier of a WebThing resource
64      * @param executor executor to use
65      * @param errorHandler the error handler
66      * @throws IOException it the WebThing can not be connected
67      */
68     ConsumedThingImpl(WebSocketClient webSocketClient, HttpClient httpClient, URI webThingURI,
69             ScheduledExecutorService executor, Consumer<String> errorHandler) throws IOException {
70         this(httpClient, webThingURI, executor, errorHandler, WebSocketConnectionFactory.instance(webSocketClient));
71     }
72
73     /**
74      * constructor
75      *
76      * @param httpClient the http client to use
77      * @param webthingUrl the identifier of a WebThing resource
78      * @param executor executor to use
79      * @param errorHandler the error handler
80      * @param webSocketConnectionFactory the Websocket connectino fctory to be used
81      * @throws IOException if the WebThing can not be connected
82      */
83     ConsumedThingImpl(HttpClient httpClient, URI webthingUrl, ScheduledExecutorService executor,
84             Consumer<String> errorHandler, WebSocketConnectionFactory webSocketConnectionFactory) throws IOException {
85         this(httpClient, webthingUrl, executor, errorHandler, webSocketConnectionFactory, DEFAULT_PING_PERIOD);
86     }
87
88     /**
89      * constructor
90      *
91      * @param httpClient the http client to use
92      * @param webthingUrl the identifier of a WebThing resource
93      * @param executor executor to use
94      * @param errorHandler the error handler
95      * @param webSocketConnectionFactory the Websocket connectino fctory to be used
96      * @param pingPeriod the ping period tothe the healthiness of the connection
97      * @throws IOException if the WebThing can not be connected
98      */
99     ConsumedThingImpl(HttpClient httpClient, URI webthingUrl, ScheduledExecutorService executor,
100             Consumer<String> errorHandler, WebSocketConnectionFactory webSocketConnectionFactory, Duration pingPeriod)
101             throws IOException {
102         this.webThingURI = webthingUrl;
103         this.httpClient = httpClient;
104         this.errorHandler = errorHandler;
105         this.description = new DescriptionLoader(httpClient).loadWebthingDescription(webThingURI,
106                 Duration.ofSeconds(20));
107
108         // opens a websocket downstream to be notified if a property value will be changed
109         var optionalEventStreamUri = this.description.getEventStreamUri();
110         if (optionalEventStreamUri.isPresent()) {
111             this.websocketDownstream = webSocketConnectionFactory.create(optionalEventStreamUri.get(), executor,
112                     this::onError, pingPeriod);
113         } else {
114             throw new IOException("WebThing " + webThingURI + " does not support websocket uri. WebThing description: "
115                     + this.description);
116         }
117     }
118
119     private Optional<URI> getPropertyUri(String propertyName) {
120         var optionalProperty = description.getProperty(propertyName);
121         if (optionalProperty.isPresent()) {
122             var propertyDescription = optionalProperty.get();
123             for (var link : propertyDescription.links) {
124                 if ((link.rel != null) && (link.href != null) && "property".equals(link.rel)) {
125                     return Optional.of(webThingURI.resolve(link.href));
126                 }
127             }
128         }
129         return Optional.empty();
130     }
131
132     @Override
133     public boolean isAlive() {
134         return isOpen.get() && this.websocketDownstream.isAlive();
135     }
136
137     @Override
138     public void close() {
139         isOpen.set(false);
140         this.websocketDownstream.close();
141     }
142
143     void onError(String reason) {
144         logger.debug("WebThing {} error occurred. {}", webThingURI, reason);
145         if (isOpen.get()) {
146             errorHandler.accept(reason);
147         }
148         close();
149     }
150
151     @Override
152     public WebThingDescription getThingDescription() {
153         return this.description;
154     }
155
156     @Override
157     public void observeProperty(String propertyName, BiConsumer<String, Object> listener) {
158         this.websocketDownstream.observeProperty(propertyName, listener);
159
160         // it may take a long time before the observed property value will be changed. For this reason
161         // read and notify the current property value (as starting point)
162         try {
163             var value = readProperty(propertyName);
164             listener.accept(propertyName, value);
165         } catch (PropertyAccessException pae) {
166             logger.warn("could not read WebThing {} property {}", webThingURI, propertyName, pae);
167         }
168     }
169
170     @Override
171     public Object readProperty(String propertyName) throws PropertyAccessException {
172         var optionalPropertyUri = getPropertyUri(propertyName);
173         if (optionalPropertyUri.isPresent()) {
174             var propertyUri = optionalPropertyUri.get();
175             try {
176                 var response = httpClient.newRequest(propertyUri).timeout(30, TimeUnit.SECONDS)
177                         .accept("application/json").send();
178                 if (response.getStatus() < 200 || response.getStatus() >= 300) {
179                     onError("WebThing " + webThingURI + " disconnected");
180                     throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri + ")");
181                 }
182                 var body = response.getContentAsString();
183                 var properties = gson.fromJson(body, Map.class);
184                 if (properties == null) {
185                     onError("WebThing " + webThingURI + " erroneous");
186                     throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri
187                             + "). Response does not include any property (" + propertyUri + "): " + body);
188                 } else {
189                     var value = properties.get(propertyName);
190                     if (value != null) {
191                         return value;
192                     } else {
193                         onError("WebThing " + webThingURI + " erroneous");
194                         throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri
195                                 + "). Response does not include " + propertyName + "(" + propertyUri + "): " + body);
196                     }
197                 }
198             } catch (ExecutionException | TimeoutException | InterruptedException e) {
199                 onError("WebThing resource " + webThingURI + " disconnected");
200                 throw new PropertyAccessException("could not read " + propertyName + " (" + propertyUri + ").", e);
201             }
202         } else {
203             onError("WebThing " + webThingURI + " does not support " + propertyName);
204             throw new PropertyAccessException("WebThing " + webThingURI + " does not support " + propertyName);
205         }
206     }
207
208     @Override
209     public void writeProperty(String propertyName, Object newValue) throws PropertyAccessException {
210         var optionalPropertyUri = getPropertyUri(propertyName);
211         if (optionalPropertyUri.isPresent()) {
212             var propertyUri = optionalPropertyUri.get();
213             var optionalProperty = description.getProperty(propertyName);
214             if (optionalProperty.isPresent()) {
215                 try {
216                     if (optionalProperty.get().readOnly) {
217                         throw new PropertyAccessException("could not write " + propertyName + " (" + propertyUri
218                                 + ") with " + newValue + ". Property is readOnly");
219                     } else {
220                         logger.debug("updating {} with {}", propertyName, newValue);
221                         Map<String, Object> payload = Map.of(propertyName, newValue);
222                         var json = gson.toJson(payload);
223                         var response = httpClient.newRequest(propertyUri).method("PUT")
224                                 .content(new StringContentProvider(json), "application/json")
225                                 .timeout(30, TimeUnit.SECONDS).send();
226                         if (response.getStatus() < 200 || response.getStatus() >= 300) {
227                             onError("WebThing " + webThingURI + "could not write " + propertyName + " (" + propertyUri
228                                     + ") with " + newValue);
229                             throw new PropertyAccessException(
230                                     "could not write " + propertyName + " (" + propertyUri + ") with " + newValue);
231                         }
232                     }
233                 } catch (ExecutionException | TimeoutException | InterruptedException e) {
234                     onError("WebThing resource " + webThingURI + " disconnected");
235                     throw new PropertyAccessException(
236                             "could not write " + propertyName + " (" + propertyUri + ") with " + newValue, e);
237                 }
238             } else {
239                 throw new PropertyAccessException("could not write " + propertyName + " (" + propertyUri + ") with "
240                         + newValue + " WebTing does not support a property named " + propertyName);
241             }
242         } else {
243             onError("WebThing " + webThingURI + " does not support " + propertyName);
244             throw new PropertyAccessException("WebThing " + webThingURI + " does not support " + propertyName);
245         }
246     }
247
248     /**
249      * Gets the property description
250      *
251      * @param propertyName the propertyName
252      * @return the description (meta data) of the property
253      */
254     public @Nullable Property getPropertyDescription(String propertyName) {
255         return description.properties.get(propertyName);
256     }
257
258     @Override
259     public String toString() {
260         return "WebThing " + description.title + " (" + webThingURI + ")";
261     }
262 }