2 * Copyright (c) 2010-2022 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.siemensrds.internal;
15 import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
17 import java.io.BufferedReader;
18 import java.io.DataOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.InputStreamReader;
22 import java.io.OutputStream;
23 import java.net.HttpURLConnection;
24 import java.net.MalformedURLException;
25 import java.net.ProtocolException;
27 import java.nio.charset.StandardCharsets;
28 import java.util.HashMap;
29 import java.util.HashSet;
31 import java.util.Map.Entry;
34 import javax.net.ssl.HttpsURLConnection;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.siemensrds.points.BasePoint;
39 import org.openhab.binding.siemensrds.points.PointDeserializer;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.Gson;
44 import com.google.gson.GsonBuilder;
45 import com.google.gson.JsonParseException;
46 import com.google.gson.annotations.SerializedName;
50 * Interface to the Data Points of a particular Plant
52 * @author Andrew Fiddian-Green - Initial contribution
56 public class RdsDataPoints {
59 * NOTE: requires a static logger because the class has static methods
61 protected final Logger logger = LoggerFactory.getLogger(RdsDataPoints.class);
63 private static final Gson GSON = new GsonBuilder().registerTypeAdapter(BasePoint.class, new PointDeserializer())
67 * this is a second index into to the JSON "values" points Map below; the
68 * purpose is to allow point lookups by a) pointId (which we do directly from
69 * the Map, and b) by pointClass (which we do indirectly "double dereferenced"
72 private final Map<String, String> indexClassToId = new HashMap<>();
74 @SerializedName("totalCount")
75 private @Nullable String totalCount;
76 @SerializedName("values")
77 public @Nullable Map<String, @Nullable BasePoint> points;
79 private String valueFilter = "";
82 * protected static method: can be used by this class and by other classes to
83 * execute an HTTP GET command on the remote cloud server to retrieve the JSON
84 * response from the given urlString
86 protected static String httpGenericGetJson(String apiKey, String token, String urlString) throws IOException {
88 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
89 * preferred JETTY library; the reason is that JETTY does not allow sending the
90 * square brackets characters "[]" verbatim over HTTP connections
92 URL url = new URL(urlString);
93 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
95 https.setRequestMethod(HTTP_GET);
97 https.setRequestProperty(USER_AGENT, MOZILLA);
98 https.setRequestProperty(ACCEPT, APPLICATION_JSON);
99 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
100 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
102 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
103 throw new IOException(https.getResponseMessage());
106 try (InputStream inputStream = https.getInputStream();
107 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
108 BufferedReader reader = new BufferedReader(inputStreamReader)) {
110 StringBuffer response = new StringBuffer();
111 while ((inputString = reader.readLine()) != null) {
112 response.append(inputString);
114 return response.toString();
119 * public static method: parse the JSON, and create a real instance of this
120 * class that encapsulates the data data point values
122 public static @Nullable RdsDataPoints createFromJson(String json) {
123 return GSON.fromJson(json, RdsDataPoints.class);
127 * private method: execute an HTTP PUT on the server to set a data point value
129 private void httpSetPointValueJson(String apiKey, String token, String pointUrl, String json)
130 throws RdsCloudException, ProtocolException, MalformedURLException, IOException {
132 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
133 * preferred JETTY library; the reason is that JETTY does not allow sending the
134 * square brackets characters "[]" verbatim over HTTP connections
136 URL url = new URL(pointUrl);
138 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
140 https.setRequestMethod(HTTP_PUT);
142 https.setRequestProperty(USER_AGENT, MOZILLA);
143 https.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
144 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
145 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
147 https.setDoOutput(true);
149 try (OutputStream outputStream = https.getOutputStream();
150 DataOutputStream writer = new DataOutputStream(outputStream)) {
151 writer.writeBytes(json);
154 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
155 throw new IOException(https.getResponseMessage());
160 * public method: retrieve the data point with the given pointClass
162 public BasePoint getPointByClass(String pointClass) throws RdsCloudException {
163 if (indexClassToId.isEmpty()) {
164 initClassToIdNameIndex();
167 String pointId = indexClassToId.get(pointClass);
168 if (pointId != null) {
169 return getPointById(pointId);
171 throw new RdsCloudException(String.format("pointClass \"%s\" not found", pointClass));
175 * public method: retrieve the data point with the given pointId
177 public BasePoint getPointById(String pointId) throws RdsCloudException {
178 Map<String, @Nullable BasePoint> points = this.points;
179 if (points != null) {
181 BasePoint point = points.get(pointId);
186 throw new RdsCloudException(String.format("pointId \"%s\" not found", pointId));
190 * private method: retrieve Id of data point with the given pointClass
192 public String pointClassToId(String pointClass) throws RdsCloudException {
193 if (indexClassToId.isEmpty()) {
194 initClassToIdNameIndex();
197 String pointId = indexClassToId.get(pointClass);
198 if (pointId != null && !pointId.isEmpty()) {
201 throw new RdsCloudException(String.format("no pointId to match pointClass \"%s\"", pointClass));
205 * public method: return the state of the "Online" data point
207 public boolean isOnline() throws RdsCloudException {
208 BasePoint point = getPointByClass(HIE_ONLINE);
209 return "Online".equals(point.getEnum().toString());
213 * public method: set a new data point value on the server
215 public void setValue(String apiKey, String token, String pointClass, String value) {
217 String pointId = pointClassToId(pointClass);
218 BasePoint point = getPointByClass(pointClass);
220 String url = String.format(URL_SETVAL, pointId);
221 String payload = point.commandJson(value);
223 if (logger.isTraceEnabled()) {
224 logger.trace(LOG_HTTP_COMMAND, HTTP_PUT, url.length());
225 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
226 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, payload);
227 } else if (logger.isDebugEnabled()) {
228 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_PUT, url.length());
229 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
230 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK,
231 payload.substring(0, Math.min(payload.length(), 30)));
234 httpSetPointValueJson(apiKey, token, url, payload);
235 } catch (RdsCloudException e) {
236 logger.warn(LOG_SYSTEM_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
237 } catch (JsonParseException | IOException e) {
238 logger.warn(LOG_RUNTIME_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
243 * public method: refresh the data point value from the server
245 public boolean refresh(String apiKey, String token) {
247 // initialize the value filter
248 if (valueFilter.isEmpty()) {
249 Set<String> set = new HashSet<>();
252 for (ChannelMap channel : CHAN_MAP) {
254 pointId = pointClassToId(channel.clazz);
255 set.add(String.format("\"%s\"", pointId));
256 } catch (RdsCloudException e) {
257 logger.debug("{} \"{}\" not implemented; don't include in request", channel.id, channel.clazz);
261 Map<String, @Nullable BasePoint> points = this.points;
262 if (points != null) {
263 for (Map.Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
265 BasePoint point = entry.getValue();
267 if ("Online".equals(point.getMemberName())) {
268 set.add(String.format("\"%s\"", entry.getKey()));
275 valueFilter = String.join(",", set);
278 String url = String.format(URL_VALUES, valueFilter);
280 if (logger.isTraceEnabled()) {
281 logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
282 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
283 } else if (logger.isDebugEnabled()) {
284 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
285 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
288 String json = httpGenericGetJson(apiKey, token, url);
290 if (logger.isTraceEnabled()) {
291 logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
292 logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
293 } else if (logger.isDebugEnabled()) {
294 logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
295 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
299 RdsDataPoints newPoints = GSON.fromJson(json, RdsDataPoints.class);
301 Map<String, @Nullable BasePoint> newPointsMap = newPoints != null ? newPoints.points : null;
303 if (newPointsMap == null) {
304 throw new RdsCloudException("new points map empty");
307 synchronized (this) {
308 for (Entry<String, @Nullable BasePoint> entry : newPointsMap.entrySet()) {
310 String pointId = entry.getKey();
313 BasePoint newPoint = entry.getValue();
314 if (newPoint == null) {
315 throw new RdsCloudException("invalid new point");
319 BasePoint myPoint = getPointById(pointId);
321 if (!(newPoint.getClass().equals(myPoint.getClass()))) {
322 throw new RdsCloudException("existing vs. new point class mismatch");
325 myPoint.refreshValueFrom(newPoint);
327 if (logger.isDebugEnabled()) {
328 logger.debug("refresh {}.{}: {} << {}", getDescription(), myPoint.getPointClass(),
329 myPoint.getState(), newPoint.getState());
335 } catch (RdsCloudException e) {
336 logger.warn(LOG_SYSTEM_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
337 } catch (JsonParseException | IOException e) {
338 logger.warn(LOG_RUNTIME_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
344 * initialize the second index into to the points Map
346 private void initClassToIdNameIndex() {
347 Map<String, @Nullable BasePoint> points = this.points;
348 if (points != null) {
349 indexClassToId.clear();
350 for (Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
351 String pointKey = entry.getKey();
352 BasePoint pointValue = entry.getValue();
353 if (pointValue != null) {
354 indexClassToId.put(pointValue.getPointClass(), pointKey);
361 * public method: return the state of the "Description" data point
363 public String getDescription() throws RdsCloudException {
364 return getPointByClass(HIE_DESCRIPTION).getState().toString();