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)
87 throws RdsCloudException, IOException {
89 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
90 * preferred JETTY library; the reason is that JETTY does not allow sending the
91 * square brackets characters "[]" verbatim over HTTP connections
93 URL url = new URL(urlString);
94 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
96 https.setRequestMethod(HTTP_GET);
98 https.setRequestProperty(USER_AGENT, MOZILLA);
99 https.setRequestProperty(ACCEPT, APPLICATION_JSON);
100 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
101 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
103 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
104 throw new IOException(https.getResponseMessage());
107 try (InputStream inputStream = https.getInputStream();
108 InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
109 BufferedReader reader = new BufferedReader(inputStreamReader)) {
111 StringBuffer response = new StringBuffer();
112 while ((inputString = reader.readLine()) != null) {
113 response.append(inputString);
115 return response.toString();
120 * public static method: parse the JSON, and create a real instance of this
121 * class that encapsulates the data data point values
123 public static @Nullable RdsDataPoints createFromJson(String json) {
124 return GSON.fromJson(json, RdsDataPoints.class);
128 * private method: execute an HTTP PUT on the server to set a data point value
130 private void httpSetPointValueJson(String apiKey, String token, String pointUrl, String json)
131 throws RdsCloudException, ProtocolException, MalformedURLException, IOException {
133 * NOTE: this class uses JAVAX HttpsURLConnection library instead of the
134 * preferred JETTY library; the reason is that JETTY does not allow sending the
135 * square brackets characters "[]" verbatim over HTTP connections
137 URL url = new URL(pointUrl);
139 HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
141 https.setRequestMethod(HTTP_PUT);
143 https.setRequestProperty(USER_AGENT, MOZILLA);
144 https.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
145 https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
146 https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
148 https.setDoOutput(true);
150 try (OutputStream outputStream = https.getOutputStream();
151 DataOutputStream writer = new DataOutputStream(outputStream)) {
152 writer.writeBytes(json);
155 if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
156 throw new IOException(https.getResponseMessage());
161 * public method: retrieve the data point with the given pointClass
163 public BasePoint getPointByClass(String pointClass) throws RdsCloudException {
164 if (indexClassToId.isEmpty()) {
165 initClassToIdNameIndex();
168 String pointId = indexClassToId.get(pointClass);
169 if (pointId != null) {
170 return getPointById(pointId);
172 throw new RdsCloudException(String.format("pointClass \"%s\" not found", pointClass));
176 * public method: retrieve the data point with the given pointId
178 public BasePoint getPointById(String pointId) throws RdsCloudException {
179 Map<String, @Nullable BasePoint> points = this.points;
180 if (points != null) {
182 BasePoint point = points.get(pointId);
187 throw new RdsCloudException(String.format("pointId \"%s\" not found", pointId));
191 * private method: retrieve Id of data point with the given pointClass
193 public String pointClassToId(String pointClass) throws RdsCloudException {
194 if (indexClassToId.isEmpty()) {
195 initClassToIdNameIndex();
198 String pointId = indexClassToId.get(pointClass);
199 if (pointId != null) {
202 throw new RdsCloudException(String.format("no pointId to match pointClass \"%s\"", pointClass));
206 * public method: return the state of the "Online" data point
208 public boolean isOnline() throws RdsCloudException {
209 BasePoint point = getPointByClass(HIE_ONLINE);
210 return "Online".equals(point.getEnum().toString());
214 * public method: set a new data point value on the server
216 public void setValue(String apiKey, String token, String pointClass, String value) {
218 String pointId = pointClassToId(pointClass);
219 BasePoint point = getPointByClass(pointClass);
221 String url = String.format(URL_SETVAL, pointId);
222 String payload = point.commandJson(value);
224 if (logger.isTraceEnabled()) {
225 logger.trace(LOG_HTTP_COMMAND, HTTP_PUT, url.length());
226 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
227 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, payload);
228 } else if (logger.isDebugEnabled()) {
229 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_PUT, url.length());
230 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
231 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK,
232 payload.substring(0, Math.min(payload.length(), 30)));
235 httpSetPointValueJson(apiKey, token, url, payload);
236 } catch (RdsCloudException e) {
237 logger.warn(LOG_SYSTEM_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
238 } catch (JsonParseException | IOException e) {
239 logger.warn(LOG_RUNTIME_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
244 * public method: refresh the data point value from the server
246 public boolean refresh(String apiKey, String token) {
248 // initialize the value filter
249 if (valueFilter.isEmpty()) {
250 Set<String> set = new HashSet<>();
253 for (ChannelMap chan : CHAN_MAP) {
254 pointId = pointClassToId(chan.clazz);
255 if (!pointId.isEmpty()) {
256 set.add(String.format("\"%s\"", pointId));
260 Map<String, @Nullable BasePoint> points = this.points;
261 if (points != null) {
262 for (Map.Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
264 BasePoint point = entry.getValue();
266 if ("Online".equals(point.getMemberName())) {
267 set.add(String.format("\"%s\"", entry.getKey()));
274 valueFilter = String.join(",", set);
277 String url = String.format(URL_VALUES, valueFilter);
279 if (logger.isTraceEnabled()) {
280 logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
281 logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
282 } else if (logger.isDebugEnabled()) {
283 logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
284 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
287 String json = httpGenericGetJson(apiKey, token, url);
289 if (logger.isTraceEnabled()) {
290 logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
291 logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
292 } else if (logger.isDebugEnabled()) {
293 logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
294 logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
298 RdsDataPoints newPoints = GSON.fromJson(json, RdsDataPoints.class);
300 Map<String, @Nullable BasePoint> newPointsMap = newPoints != null ? newPoints.points : null;
302 if (newPointsMap == null) {
303 throw new RdsCloudException("new points map empty");
306 synchronized (this) {
307 for (Entry<String, @Nullable BasePoint> entry : newPointsMap.entrySet()) {
309 String pointId = entry.getKey();
312 BasePoint newPoint = entry.getValue();
313 if (newPoint == null) {
314 throw new RdsCloudException("invalid new point");
318 BasePoint myPoint = getPointById(pointId);
320 if (!(newPoint.getClass().equals(myPoint.getClass()))) {
321 throw new RdsCloudException("existing vs. new point class mismatch");
324 myPoint.refreshValueFrom(newPoint);
326 if (logger.isDebugEnabled()) {
327 logger.debug("refresh {}.{}: {} << {}", getDescription(), myPoint.getPointClass(),
328 myPoint.getState(), newPoint.getState());
334 } catch (RdsCloudException e) {
335 logger.warn(LOG_SYSTEM_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
336 } catch (JsonParseException | IOException e) {
337 logger.warn(LOG_RUNTIME_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
343 * initialize the second index into to the points Map
345 private void initClassToIdNameIndex() {
346 Map<String, @Nullable BasePoint> points = this.points;
347 if (points != null) {
348 indexClassToId.clear();
349 for (Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
350 String pointKey = entry.getKey();
351 BasePoint pointValue = entry.getValue();
352 if (pointValue != null) {
353 indexClassToId.put(pointValue.getPointClass(), pointKey);
360 * public method: return the state of the "Description" data point
362 public String getDescription() throws RdsCloudException {
363 return getPointByClass(HIE_DESCRIPTION).getState().toString();