2 * Copyright (c) 2010-2021 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.io.UnsupportedEncodingException;
24 import java.net.HttpURLConnection;
25 import java.net.MalformedURLException;
26 import java.net.ProtocolException;
28 import java.nio.charset.StandardCharsets;
29 import java.util.HashMap;
30 import java.util.HashSet;
32 import java.util.Map.Entry;
35 import javax.net.ssl.HttpsURLConnection;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.siemensrds.points.BasePoint;
40 import org.openhab.binding.siemensrds.points.PointDeserializer;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonParseException;
47 import com.google.gson.annotations.SerializedName;
51 * Interface to the Data Points of a particular Plant
53 * @author Andrew Fiddian-Green - Initial contribution
57 public class RdsDataPoints {
60 * NOTE: requires a static logger because the class has static methods
62 protected final Logger logger = LoggerFactory.getLogger(RdsDataPoints.class);
64 private static final Gson GSON = new GsonBuilder().registerTypeAdapter(BasePoint.class, new PointDeserializer())
68 * this is a second index into to the JSON "values" points Map below; the
69 * purpose is to allow point lookups by a) pointId (which we do directly from
70 * the Map, and b) by pointClass (which we do indirectly "double dereferenced"
73 private final Map<String, String> indexClassToId = new HashMap<>();
75 @SerializedName("totalCount")
76 private @Nullable String totalCount;
77 @SerializedName("values")
78 public @Nullable Map<String, @Nullable BasePoint> points;
80 private String valueFilter = "";
83 * protected static method: can be used by this class and by other classes to
84 * execute an HTTP GET command on the remote cloud server to retrieve the JSON
85 * response from the given urlString
87 protected static String httpGenericGetJson(String apiKey, String token, String urlString)
88 throws RdsCloudException, IOException {
90 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
91 * preferred JETTY library; the reason is that JETTY does not allow sending the
92 * square brackets characters "[]" verbatim over HTTP connections
94 URL url = new URL(urlString);
95 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
97 https.setRequestMethod(HTTP_GET);
99 https.setRequestProperty(USER_AGENT, MOZILLA);
100 https.setRequestProperty(ACCEPT, APPLICATION_JSON);
101 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
102 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
104 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
105 throw new IOException(https.getResponseMessage());
108 try (InputStream inputStream = https.getInputStream();
109 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
110 BufferedReader reader = new BufferedReader(inputStreamReader)) {
112 StringBuffer response = new StringBuffer();
113 while ((inputString = reader.readLine()) != null) {
114 response.append(inputString);
116 return response.toString();
121 * public static method: parse the JSON, and create a real instance of this
122 * class that encapsulates the data data point values
124 public static @Nullable RdsDataPoints createFromJson(String json) {
125 return GSON.fromJson(json, RdsDataPoints.class);
129 * private method: execute an HTTP PUT on the server to set a data point value
131 private void httpSetPointValueJson(String apiKey, String token, String pointUrl, String json)
132 throws RdsCloudException, UnsupportedEncodingException, ProtocolException, MalformedURLException,
135 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
136 * preferred JETTY library; the reason is that JETTY does not allow sending the
137 * square brackets characters "[]" verbatim over HTTP connections
139 URL url = new URL(pointUrl);
141 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
143 https.setRequestMethod(HTTP_PUT);
145 https.setRequestProperty(USER_AGENT, MOZILLA);
146 https.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
147 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
148 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
150 https.setDoOutput(true);
152 try (OutputStream outputStream = https.getOutputStream();
153 DataOutputStream writer = new DataOutputStream(outputStream)) {
154 writer.writeBytes(json);
157 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
158 throw new IOException(https.getResponseMessage());
163 * public method: retrieve the data point with the given pointClass
165 public BasePoint getPointByClass(String pointClass) throws RdsCloudException {
166 if (indexClassToId.isEmpty()) {
167 initClassToIdNameIndex();
170 String pointId = indexClassToId.get(pointClass);
171 if (pointId != null) {
172 return getPointById(pointId);
174 throw new RdsCloudException(String.format("pointClass \"%s\" not found", pointClass));
178 * public method: retrieve the data point with the given pointId
180 public BasePoint getPointById(String pointId) throws RdsCloudException {
181 Map<String, @Nullable BasePoint> points = this.points;
182 if (points != null) {
184 BasePoint point = points.get(pointId);
189 throw new RdsCloudException(String.format("pointId \"%s\" not found", pointId));
193 * private method: retrieve Id of data point with the given pointClass
195 public String pointClassToId(String pointClass) throws RdsCloudException {
196 if (indexClassToId.isEmpty()) {
197 initClassToIdNameIndex();
200 String pointId = indexClassToId.get(pointClass);
201 if (pointId != null) {
204 throw new RdsCloudException(String.format("no pointId to match pointClass \"%s\"", pointClass));
208 * public method: return the state of the "Online" data point
210 public boolean isOnline() throws RdsCloudException {
211 BasePoint point = getPointByClass(HIE_ONLINE);
212 return "Online".equals(point.getEnum().toString());
216 * public method: set a new data point value on the server
218 public void setValue(String apiKey, String token, String pointClass, String value) {
220 String pointId = pointClassToId(pointClass);
221 BasePoint point = getPointByClass(pointClass);
223 String url = String.format(URL_SETVAL, pointId);
224 String payload = point.commandJson(value);
226 if (logger.isTraceEnabled()) {
227 logger.trace(LOG_HTTP_COMMAND, HTTP_PUT, url.length());
228 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
229 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, payload);
230 } else if (logger.isDebugEnabled()) {
231 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_PUT, url.length());
232 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
233 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK,
234 payload.substring(0, Math.min(payload.length(), 30)));
237 httpSetPointValueJson(apiKey, token, url, payload);
238 } catch (RdsCloudException e) {
239 logger.warn(LOG_SYSTEM_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
240 } catch (JsonParseException | IOException e) {
241 logger.warn(LOG_RUNTIME_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
246 * public method: refresh the data point value from the server
248 public boolean refresh(String apiKey, String token) {
250 // initialize the value filter
251 if (valueFilter.isEmpty()) {
252 Set<String> set = new HashSet<>();
255 for (ChannelMap chan : CHAN_MAP) {
256 pointId = pointClassToId(chan.clazz);
257 if (!pointId.isEmpty()) {
258 set.add(String.format("\"%s\"", pointId));
262 Map<String, @Nullable BasePoint> points = this.points;
263 if (points != null) {
264 for (Map.Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
266 BasePoint point = entry.getValue();
268 if ("Online".equals(point.getMemberName())) {
269 set.add(String.format("\"%s\"", entry.getKey()));
276 valueFilter = String.join(",", set);
279 String url = String.format(URL_VALUES, valueFilter);
281 if (logger.isTraceEnabled()) {
282 logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
283 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
284 } else if (logger.isDebugEnabled()) {
285 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
286 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
289 String json = httpGenericGetJson(apiKey, token, url);
291 if (logger.isTraceEnabled()) {
292 logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
293 logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
294 } else if (logger.isDebugEnabled()) {
295 logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
296 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
300 RdsDataPoints newPoints = GSON.fromJson(json, RdsDataPoints.class);
302 Map<String, @Nullable BasePoint> newPointsMap = newPoints != null ? newPoints.points : null;
304 if (newPointsMap == null) {
305 throw new RdsCloudException("new points map empty");
308 synchronized (this) {
309 for (Entry<String, @Nullable BasePoint> entry : newPointsMap.entrySet()) {
311 String pointId = entry.getKey();
314 BasePoint newPoint = entry.getValue();
315 if (newPoint == null) {
316 throw new RdsCloudException("invalid new point");
320 BasePoint myPoint = getPointById(pointId);
322 if (!(newPoint.getClass().equals(myPoint.getClass()))) {
323 throw new RdsCloudException("existing vs. new point class mismatch");
326 myPoint.refreshValueFrom(newPoint);
328 if (logger.isDebugEnabled()) {
329 logger.debug("refresh {}.{}: {} << {}", getDescription(), myPoint.getPointClass(),
330 myPoint.getState(), newPoint.getState());
336 } catch (RdsCloudException e) {
337 logger.warn(LOG_SYSTEM_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
338 } catch (JsonParseException | IOException e) {
339 logger.warn(LOG_RUNTIME_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
345 * initialize the second index into to the points Map
347 private void initClassToIdNameIndex() {
348 Map<String, @Nullable BasePoint> points = this.points;
349 if (points != null) {
350 indexClassToId.clear();
351 for (Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
352 String pointKey = entry.getKey();
353 BasePoint pointValue = entry.getValue();
354 if (pointValue != null) {
355 indexClassToId.put(pointValue.getPointClass(), pointKey);
362 * public method: return the state of the "Description" data point
364 public String getDescription() throws RdsCloudException {
365 return getPointByClass(HIE_DESCRIPTION).getState().toString();