added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.neohub-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-siemensrds" description="Siemens RDS Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.siemensrds/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import javax.net.ssl.HttpsURLConnection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
* Interface to the Access Token for a particular User
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsAccessToken {
/*
* NOTE: requires a static logger because the class has static methods
*/
protected final Logger logger = LoggerFactory.getLogger(RdsAccessToken.class);
private static final Gson GSON = new Gson();
@SerializedName("access_token")
private @Nullable String accessToken;
@SerializedName(".expires")
private @Nullable String expires;
private @Nullable Date expDate = null;
/*
* public static method: execute the HTTP POST on the server
*/
public static String httpGetTokenJson(String apiKey, String payload) throws RdsCloudException, IOException {
/*
* NOTE: this class uses JAVAX HttpsURLConnection library instead of the
* preferred JETTY library; the reason is that JETTY does not allow sending the
* square brackets characters "[]" verbatim over HTTP connections
*/
URL url = new URL(URL_TOKEN);
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
https.setRequestMethod(HTTP_POST);
https.setRequestProperty(USER_AGENT, MOZILLA);
https.setRequestProperty(ACCEPT, TEXT_PLAIN);
https.setRequestProperty(CONTENT_TYPE, TEXT_PLAIN);
https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
https.setDoOutput(true);
try (OutputStream outputStream = https.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
dataOutputStream.writeBytes(payload);
dataOutputStream.flush();
}
if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(https.getResponseMessage());
}
try (InputStream inputStream = https.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(inputStreamReader)) {
String inputString;
StringBuffer response = new StringBuffer();
while ((inputString = reader.readLine()) != null) {
response.append(inputString);
}
return response.toString();
}
}
/*
* public method: parse the JSON, and create a class that encapsulates the data
*/
public static @Nullable RdsAccessToken createFromJson(String json) {
return GSON.fromJson(json, RdsAccessToken.class);
}
/*
* public method: return the access token
*/
public String getToken() throws RdsCloudException {
String accessToken = this.accessToken;
if (accessToken != null) {
return accessToken;
}
throw new RdsCloudException("no access token");
}
/*
* public method: check if the token has expired
*/
public boolean isExpired() {
Date expDate = this.expDate;
if (expDate == null) {
try {
expDate = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z").parse(expires);
} catch (ParseException e) {
logger.debug("isExpired: expiry date parsing exception");
Calendar calendar = Calendar.getInstance();
calendar.setTime(new Date());
calendar.add(Calendar.DAY_OF_YEAR, 1);
expDate = calendar.getTime();
}
}
return (expDate == null || expDate.before(new Date()));
}
}

View File

@@ -0,0 +1,258 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link RdsBindingConstants} contains the constants used by the binding
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class RdsBindingConstants {
/*
* binding id
*/
public static final String BINDING_ID = "siemensrds";
/*
* device id's
*/
public static final String DEVICE_ID_CLOUD = "climatixic";
public static final String DEVICE_ID_STAT = "rds";
/*
* Thing Type UIDs
*/
public static final ThingTypeUID THING_TYPE_CLOUD = new ThingTypeUID(BINDING_ID, DEVICE_ID_CLOUD);
public static final ThingTypeUID THING_TYPE_RDS = new ThingTypeUID(BINDING_ID, DEVICE_ID_STAT);
// ========================== URLs and HTTP stuff =========================
private static final String API = "https://api.climatixic.com/";
private static final String ARG_RDS = "?filterId=[" + "{\"asn\":\"RDS110\"}," + "{\"asn\":\"RDS120\"},"
+ "{\"asn\":\"RDS110.R\"}," + "{\"asn\":\"RDS120.B\"}" + "]";
private static final String ARG_PARENT = "?parentId=[\"%s\"]&take=100";
private static final String ARG_POINT = "?filterId=[%s]";
public static final String URL_TOKEN = API + "Token";
public static final String URL_PLANTS = API + "Plants" + ARG_RDS;
public static final String URL_POINTS = API + "DataPoints" + ARG_PARENT;
public static final String URL_SETVAL = API + "DataPoints/%s";
public static final String URL_VALUES = API + "DataPoints/Values" + ARG_POINT;
public static final String HTTP_POST = "POST";
public static final String HTTP_GET = "GET";
public static final String HTTP_PUT = "PUT";
public static final String USER_AGENT = "User-Agent";
public static final String ACCEPT = "Accept";
public static final String CONTENT_TYPE = "Content-Type";
public static final String MOZILLA = "Mozilla/5.0";
public static final String APPLICATION_JSON = "application/json;charset=UTF-8";
public static final String TEXT_PLAIN = "text/plain;charset=UTF-8";
public static final String SUBSCRIPTION_KEY = "Ocp-Apim-Subscription-Key";
public static final String AUTHORIZATION = "Authorization";
public static final String BEARER = "Bearer %s";
public static final String TOKEN_REQUEST = "grant_type=password&username=%s&password=%s&expire_minutes=20160";
/*
* setup parameters for de-bouncing of state changes (time in seconds) so state
* changes that occur within this time window are ignored
*/
public static final long DEBOUNCE_DELAY = 15;
/*
* setup parameters for lazy polling
*/
public static final int LAZY_POLL_INTERVAL = 60;
/*
* setup parameters for fast polling bursts a burst comprises FAST_POLL_CYCLES
* polling calls spaced at FAST_POLL_INTERVAL for example 5 polling calls made
* at 4 second intervals (e.g. 6 x 4 => 24 seconds)
*/
public static final int FAST_POLL_CYCLES = 6;
public static final int FAST_POLL_INTERVAL = 8;
/*
* setup parameters for device discovery
*/
public static final int DISCOVERY_TIMEOUT = 5;
public static final int DISCOVERY_START_DELAY = 30;
public static final int DISCOVERY_REFRESH_PERIOD = 600;
public static final String PROP_PLANT_ID = "plantId";
/*
* ==================== USED DATA POINTS ==========================
*
* where: HIE_xxx = the point class suffix part of the hierarchy name in the
* ClimatixIc server, and CHA_xxx = the Channel ID in the OpenHAB binding
*
*/
// device name
public static final String HIE_DESCRIPTION = "'Description";
// online state
public static final String HIE_ONLINE = "#Online";
// room (actual) temperature (read-only)
protected static final String CHA_ROOM_TEMP = "roomTemperature";
public static final String HIE_ROOM_TEMP = "'RTemp";
// room relative humidity (read-only)
protected static final String CHA_ROOM_HUMIDITY = "roomHumidity";
public static final String HIE_ROOM_HUMIDITY = "'RHuRel";
// room air quality (low/med/high) (read-only)
protected static final String CHA_ROOM_AIR_QUALITY = "roomAirQuality";
public static final String HIE_ROOM_AIR_QUALITY = "'RAQualInd";
// energy savings level (green leaf) (poor..excellent) (read-write)
// note: writing the value "5" forces the device to green leaf mode
protected static final String CHA_ENERGY_SAVINGS_LEVEL = "energySavingsLevel";
public static final String HIE_ENERGY_SAVINGS_LEVEL = "'REei";
// outside air temperature (read-only)
protected static final String CHA_OUTSIDE_TEMP = "outsideTemperature";
public static final String HIE_OUTSIDE_TEMP = "'TOa";
// set-point override (read-write)
protected static final String CHA_TARGET_TEMP = "targetTemperature";
public static final String HIE_TARGET_TEMP = "'SpTR";
// heating/cooling state (read-only)
protected static final String CHA_OUTPUT_STATE = "thermostatOutputState";
public static final String HIE_OUTPUT_STATE = "'HCSta";
/*
* thermostat occupancy state (absent, present) (read-write) NOTE: uses
* different parameters as follows.. OccMod = 2, 3 to read, and command to, the
* absent, present states
*/
protected static final String CHA_STAT_OCC_MODE_PRESENT = "occupancyModePresent";
public static final String HIE_STAT_OCC_MODE_PRESENT = "'OccMod";
/*
* thermostat program mode (read-write) NOTE: uses different parameters as
* follows.. PrOpModRsn presentPriority < / > 13 combined with OccMod = 2 to
* read the manual, auto mode CmfBtn = 1 to command to the manual mode REei = 5
* to command to the auto mode
*/
protected static final String CHA_STAT_AUTO_MODE = "thermostatAutoMode";
public static final String HIE_PR_OP_MOD_RSN = "'PrOpModRsn";
public static final String HIE_STAT_CMF_BTN = "'CmfBtn";
/*
* domestic hot water state (off, on) (read-write) NOTE: uses different
* parameters as follows.. DhwMod = 1, 2 to read, and command to, the off, on
* states
*/
protected static final String CHA_DHW_OUTPUT_STATE = "hotWaterOutputState";
public static final String HIE_DHW_OUTPUT_STATE = "'DhwMod";
/*
* domestic hot water program mode (manual, auto) (read-write) NOTE: uses
* different parameters as follows.. DhwMod presentPriority < / > 13 to read the
* manual, auto mode DhwMod = 0 to command to the auto mode DhwMod = its current
* value to command it's resp. manual states
*/
protected static final String CHA_DHW_AUTO_MODE = "hotWaterAutoMode";
/*
* openHAB status strings
*/
protected static final String STATE_NEITHER = "Neither";
protected static final String STATE_OFF = "Off";
public static final ChannelMap[] CHAN_MAP = { new ChannelMap(CHA_ROOM_TEMP, HIE_ROOM_TEMP),
new ChannelMap(CHA_ROOM_HUMIDITY, HIE_ROOM_HUMIDITY), new ChannelMap(CHA_OUTSIDE_TEMP, HIE_OUTSIDE_TEMP),
new ChannelMap(CHA_TARGET_TEMP, HIE_TARGET_TEMP),
new ChannelMap(CHA_ROOM_AIR_QUALITY, HIE_ROOM_AIR_QUALITY),
new ChannelMap(CHA_ENERGY_SAVINGS_LEVEL, HIE_ENERGY_SAVINGS_LEVEL),
new ChannelMap(CHA_OUTPUT_STATE, HIE_OUTPUT_STATE),
new ChannelMap(CHA_STAT_OCC_MODE_PRESENT, HIE_STAT_OCC_MODE_PRESENT),
new ChannelMap(CHA_STAT_AUTO_MODE, HIE_PR_OP_MOD_RSN),
new ChannelMap(CHA_DHW_OUTPUT_STATE, HIE_DHW_OUTPUT_STATE),
new ChannelMap(CHA_DHW_AUTO_MODE, HIE_DHW_OUTPUT_STATE) };
/*
* ==================== UNUSED DATA POINTS ======================
*
* room air quality (numeric value)
*
* private static final String HIE_ROOM_AIR_QUALITY_VAL = "RAQual";
*
* other set-points for phases of the time program mode
*
* private static final String HIE_CMF_SETPOINT = "SpHCmf";
*
* private static final String HIE_PCF_SETPOINT = "SpHPcf";
*
* private static final String HIE_ECO_SETPOINT = "SpHEco";
*
* private static final String HIE_PRT_SETPOINT = "SpHPrt";
*
* enable heating control
*
* private static final String HIE_ENABLE_HEATING = "EnHCtl";
*
* comfort button
*
* private static final String HIE_COMFORT_BUTTON = "CmfBtn";
*
*/
/*
* logger strings
*/
public static final String LOG_HTTP_COMMAND = "{} for url {} characters long";
public static final String LOG_CONTENT_LENGTH = "{} {} characters..";
public static final String LOG_PAYLOAD_FMT = "{} {}";
public static final String LOG_HTTP_COMMAND_ABR = "{} for url {} characters long (set log level to TRACE to see full url)..";
public static final String LOG_CONTENT_LENGTH_ABR = "{} {} characters (set log level to TRACE to see full string)..";
public static final String LOG_PAYLOAD_FMT_ABR = "{} {} ...";
public static final String LOG_RECEIVED_MSG = "received";
public static final String LOG_RECEIVED_MARK = "<<";
public static final String LOG_SENDING_MSG = "sending";
public static final String LOG_SENDING_MARK = ">>";
public static final String LOG_SYSTEM_EXCEPTION = "system exception in {}, type={}, message=\"{}\"";
public static final String LOG_RUNTIME_EXCEPTION = "runtime exception in {}, type={}, message=\"{}\"";
}
/**
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
class ChannelMap {
public String id;
public String clazz;
public ChannelMap(String channelId, String pointClass) {
this.id = channelId;
this.clazz = pointClass;
}
}

View File

@@ -0,0 +1,30 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RdsCloudConfiguration} class contains the thing configuration
* parameters for the Climatix IC cloud user account
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class RdsCloudConfiguration {
public String userEmail = "";
public String userPassword = "";
public int pollingInterval;
public String apiKey = "";
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Custom Cloud Server communication exception
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsCloudException extends Exception {
private static final long serialVersionUID = -7048044632627280917L;
public RdsCloudException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,170 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonParseException;
/**
* The {@link RdsCloudHandler} is the handler for Siemens RDS cloud account (
* also known as the Climatix IC server account )
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsCloudHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(RdsCloudHandler.class);
private @Nullable RdsCloudConfiguration config = null;
private @Nullable RdsAccessToken accessToken = null;
public RdsCloudHandler(Bridge bridge) {
super(bridge);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// there is nothing to do
}
@Override
public void initialize() {
RdsCloudConfiguration config = this.config = getConfigAs(RdsCloudConfiguration.class);
if (config.userEmail.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing email address");
return;
}
if (config.userPassword.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing password");
return;
}
if (logger.isDebugEnabled())
logger.debug("polling interval={}", config.pollingInterval);
if (config.pollingInterval < FAST_POLL_INTERVAL || config.pollingInterval > LAZY_POLL_INTERVAL) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
String.format("polling interval out of range [%d..%d]", FAST_POLL_INTERVAL, LAZY_POLL_INTERVAL));
return;
}
}
@Override
public void dispose() {
// there is nothing to do
}
/*
* public method: used by RDS smart thermostat handlers return the polling
* interval (seconds)
*/
public int getPollInterval() throws RdsCloudException {
RdsCloudConfiguration config = this.config;
if (config != null) {
return config.pollingInterval;
}
throw new RdsCloudException("missing polling interval");
}
/*
* private method: check if the current token is valid, and renew it if
* necessary
*/
private synchronized void refreshToken() {
RdsCloudConfiguration config = this.config;
RdsAccessToken accessToken = this.accessToken;
if (accessToken == null || accessToken.isExpired()) {
try {
if (config == null) {
throw new RdsCloudException("missing configuration");
}
String url = URL_TOKEN;
String payload = String.format(TOKEN_REQUEST, config.userEmail, config.userPassword);
logger.debug(LOG_HTTP_COMMAND, HTTP_POST, url.length());
logger.debug(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
logger.debug(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, payload);
String json = RdsAccessToken.httpGetTokenJson(config.apiKey, payload);
if (logger.isTraceEnabled()) {
logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK,
json.substring(0, Math.min(json.length(), 30)));
}
accessToken = this.accessToken = RdsAccessToken.createFromJson(json);
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "refreshToken()", e.getClass().getName(), e.getMessage());
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "refreshToken()", e.getClass().getName(), e.getMessage());
}
}
if (accessToken != null) {
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "cloud server responded");
}
} else {
if (getThing().getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"cloud server authentication error");
}
}
}
/*
* public method: used by RDS smart thermostat handlers to fetch the current
* token
*/
public synchronized String getToken() throws RdsCloudException {
refreshToken();
RdsAccessToken accessToken = this.accessToken;
if (accessToken != null) {
return accessToken.getToken();
}
throw new RdsCloudException("no access token");
}
public String getApiKey() throws RdsCloudException {
RdsCloudConfiguration config = this.config;
if (config != null) {
return config.apiKey;
}
throw new RdsCloudException("no api key");
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link RdsConfiguration} class contains the thing configuration
* parameters for RDS thermostats
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class RdsConfiguration {
public String plantId = "";
}

View File

@@ -0,0 +1,369 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.net.ssl.HttpsURLConnection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.siemensrds.points.BasePoint;
import org.openhab.binding.siemensrds.points.PointDeserializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
/**
*
* Interface to the Data Points of a particular Plant
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsDataPoints {
/*
* NOTE: requires a static logger because the class has static methods
*/
protected final Logger logger = LoggerFactory.getLogger(RdsDataPoints.class);
private static final Gson GSON = new GsonBuilder().registerTypeAdapter(BasePoint.class, new PointDeserializer())
.create();
/*
* this is a second index into to the JSON "values" points Map below; the
* purpose is to allow point lookups by a) pointId (which we do directly from
* the Map, and b) by pointClass (which we do indirectly "double dereferenced"
* via this index
*/
private final Map<String, @Nullable String> indexClassToId = new HashMap<>();
@SerializedName("totalCount")
private @Nullable String totalCount;
@SerializedName("values")
public @Nullable Map<String, @Nullable BasePoint> points;
private String valueFilter = "";
/*
* protected static method: can be used by this class and by other classes to
* execute an HTTP GET command on the remote cloud server to retrieve the JSON
* response from the given urlString
*/
protected static String httpGenericGetJson(String apiKey, String token, String urlString)
throws RdsCloudException, IOException {
/*
* NOTE: this class uses JAVAX HttpsURLConnection library instead of the
* preferred JETTY library; the reason is that JETTY does not allow sending the
* square brackets characters "[]" verbatim over HTTP connections
*/
URL url = new URL(urlString);
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
https.setRequestMethod(HTTP_GET);
https.setRequestProperty(USER_AGENT, MOZILLA);
https.setRequestProperty(ACCEPT, APPLICATION_JSON);
https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(https.getResponseMessage());
}
try (InputStream inputStream = https.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
BufferedReader reader = new BufferedReader(inputStreamReader)) {
String inputString;
StringBuffer response = new StringBuffer();
while ((inputString = reader.readLine()) != null) {
response.append(inputString);
}
return response.toString();
}
}
/*
* public static method: parse the JSON, and create a real instance of this
* class that encapsulates the data data point values
*/
public static @Nullable RdsDataPoints createFromJson(String json) {
return GSON.fromJson(json, RdsDataPoints.class);
}
/*
* private method: execute an HTTP PUT on the server to set a data point value
*/
private void httpSetPointValueJson(String apiKey, String token, String pointUrl, String json)
throws RdsCloudException, UnsupportedEncodingException, ProtocolException, MalformedURLException,
IOException {
/*
* NOTE: this class uses JAVAX HttpsURLConnection library instead of the
* preferred JETTY library; the reason is that JETTY does not allow sending the
* square brackets characters "[]" verbatim over HTTP connections
*/
URL url = new URL(pointUrl);
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
https.setRequestMethod(HTTP_PUT);
https.setRequestProperty(USER_AGENT, MOZILLA);
https.setRequestProperty(CONTENT_TYPE, APPLICATION_JSON);
https.setRequestProperty(SUBSCRIPTION_KEY, apiKey);
https.setRequestProperty(AUTHORIZATION, String.format(BEARER, token));
https.setDoOutput(true);
try (OutputStream outputStream = https.getOutputStream();
DataOutputStream writer = new DataOutputStream(outputStream)) {
writer.writeBytes(json);
}
if (https.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException(https.getResponseMessage());
}
}
/*
* public method: retrieve the data point with the given pointClass
*/
public BasePoint getPointByClass(String pointClass) throws RdsCloudException {
if (indexClassToId.isEmpty()) {
initClassToIdNameIndex();
}
@Nullable
String pointId = indexClassToId.get(pointClass);
if (pointId != null) {
return getPointById(pointId);
}
throw new RdsCloudException(String.format("pointClass \"%s\" not found", pointClass));
}
/*
* public method: retrieve the data point with the given pointId
*/
public BasePoint getPointById(String pointId) throws RdsCloudException {
Map<String, @Nullable BasePoint> points = this.points;
if (points != null) {
@Nullable
BasePoint point = points.get(pointId);
if (point != null) {
return point;
}
}
throw new RdsCloudException(String.format("pointId \"%s\" not found", pointId));
}
/*
* private method: retrieve Id of data point with the given pointClass
*/
public String pointClassToId(String pointClass) throws RdsCloudException {
if (indexClassToId.isEmpty()) {
initClassToIdNameIndex();
}
@Nullable
String pointId = indexClassToId.get(pointClass);
if (pointId != null) {
return pointId;
}
throw new RdsCloudException(String.format("no pointId to match pointClass \"%s\"", pointClass));
}
/*
* public method: return the state of the "Online" data point
*/
public boolean isOnline() throws RdsCloudException {
BasePoint point = getPointByClass(HIE_ONLINE);
return "Online".equals(point.getEnum().toString());
}
/*
* public method: set a new data point value on the server
*/
public void setValue(String apiKey, String token, String pointClass, String value) {
try {
String pointId = pointClassToId(pointClass);
BasePoint point = getPointByClass(pointClass);
String url = String.format(URL_SETVAL, pointId);
String payload = point.commandJson(value);
if (logger.isTraceEnabled()) {
logger.trace(LOG_HTTP_COMMAND, HTTP_PUT, url.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, payload);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_PUT, url.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK,
payload.substring(0, Math.min(payload.length(), 30)));
}
httpSetPointValueJson(apiKey, token, url, payload);
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "setValue()", e.getClass().getName(), e.getMessage());
}
}
/*
* public method: refresh the data point value from the server
*/
public boolean refresh(String apiKey, String token) {
try {
// initialize the value filter
if (valueFilter.isEmpty()) {
Set<String> set = new HashSet<>();
String pointId;
for (ChannelMap chan : CHAN_MAP) {
pointId = pointClassToId(chan.clazz);
if (!pointId.isEmpty()) {
set.add(String.format("\"%s\"", pointId));
}
}
Map<String, @Nullable BasePoint> points = this.points;
if (points != null) {
for (Map.Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
@Nullable
BasePoint point = entry.getValue();
if (point != null) {
if ("Online".equals(point.getMemberName())) {
set.add(String.format("\"%s\"", entry.getKey()));
break;
}
}
}
}
valueFilter = String.join(",", set);
}
String url = String.format(URL_VALUES, valueFilter);
if (logger.isTraceEnabled()) {
logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
}
String json = httpGenericGetJson(apiKey, token, url);
if (logger.isTraceEnabled()) {
logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
}
@Nullable
RdsDataPoints newPoints = GSON.fromJson(json, RdsDataPoints.class);
Map<String, @Nullable BasePoint> newPointsMap = newPoints.points;
if (newPointsMap == null) {
throw new RdsCloudException("new points map empty");
}
synchronized (this) {
for (Entry<String, @Nullable BasePoint> entry : newPointsMap.entrySet()) {
@Nullable
String pointId = entry.getKey();
@Nullable
BasePoint newPoint = entry.getValue();
if (newPoint == null) {
throw new RdsCloudException("invalid new point");
}
@Nullable
BasePoint myPoint = getPointById(pointId);
if (!(newPoint.getClass().equals(myPoint.getClass()))) {
throw new RdsCloudException("existing vs. new point class mismatch");
}
myPoint.refreshValueFrom((BasePoint) newPoint);
if (logger.isDebugEnabled()) {
logger.debug("refresh {}.{}: {} << {}", getDescription(), myPoint.getPointClass(),
myPoint.getState(), ((BasePoint) newPoint).getState());
}
}
}
return true;
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "refresh()", e.getClass().getName(), e.getMessage());
}
return false;
}
/*
* initialize the second index into to the points Map
*/
private void initClassToIdNameIndex() {
Map<String, @Nullable BasePoint> points = this.points;
if (points != null) {
indexClassToId.clear();
for (Entry<String, @Nullable BasePoint> entry : points.entrySet()) {
@Nullable
String pointKey = entry.getKey();
@Nullable
BasePoint pointValue = entry.getValue();
if (pointValue != null) {
indexClassToId.put(pointValue.getPointClass(), pointKey);
}
}
}
}
/*
* public method: return the state of the "Description" data point
*/
public String getDescription() throws RdsCloudException {
return getPointByClass(HIE_DESCRIPTION).getState().toString();
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.DEBOUNCE_DELAY;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link RdsDebouncer} determines if change events should be forwarded to a
* channel
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class RdsDebouncer {
private final Map<String, @Nullable DebounceDelay> channels = new HashMap<>();
@SuppressWarnings("null")
@NonNullByDefault
static class DebounceDelay {
private long expireTime;
public DebounceDelay(boolean enabled) {
if (enabled) {
expireTime = new Date().getTime() + (DEBOUNCE_DELAY * 1000);
}
}
public boolean timeExpired() {
return (expireTime < new Date().getTime());
}
}
public RdsDebouncer() {
}
public void initialize(String channelId) {
channels.put(channelId, new DebounceDelay(true));
}
public Boolean timeExpired(String channelId) {
if (channels.containsKey(channelId)) {
@Nullable
DebounceDelay debounceDelay = channels.get(channelId);
if (debounceDelay != null) {
return ((DebounceDelay) debounceDelay).timeExpired();
}
}
return true;
}
}

View File

@@ -0,0 +1,211 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.siemensrds.internal.RdsPlants.PlantInfo;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonParseException;
/**
* Discovery service for Siemens RDS thermostats
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(RdsDiscoveryService.class);
private @Nullable ScheduledFuture<?> discoveryScheduler;
private @Nullable RdsCloudHandler cloud;
public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_RDS).collect(Collectors.toSet()));
public RdsDiscoveryService(RdsCloudHandler cloud) {
// note: background discovery is enabled in the super method..
super(DISCOVERABLE_THING_TYPES_UIDS, DISCOVERY_TIMEOUT);
this.cloud = cloud;
}
public void activate() {
super.activate(null);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
RdsCloudHandler cloud = this.cloud;
if (cloud != null && cloud.getThing().getStatus() != ThingStatus.ONLINE) {
try {
cloud.getToken();
} catch (RdsCloudException e) {
logger.debug("unexpected: {} = \"{}\"", e.getClass().getName(), e.getMessage());
}
}
if (cloud != null && cloud.getThing().getStatus() == ThingStatus.ONLINE) {
discoverPlants();
}
}
@Override
protected void startBackgroundDiscovery() {
logger.debug("start background discovery..");
ScheduledFuture<?> discoveryScheduler = this.discoveryScheduler;
if (discoveryScheduler == null || discoveryScheduler.isCancelled()) {
this.discoveryScheduler = scheduler.scheduleWithFixedDelay(this::startScan, 10, DISCOVERY_REFRESH_PERIOD,
TimeUnit.SECONDS);
}
}
@Override
protected void stopBackgroundDiscovery() {
logger.debug("stop background discovery..");
ScheduledFuture<?> discoveryScheduler = this.discoveryScheduler;
if (discoveryScheduler != null && !discoveryScheduler.isCancelled()) {
discoveryScheduler.cancel(true);
this.discoveryScheduler = null;
}
}
private void discoverPlants() {
RdsCloudHandler cloud = this.cloud;
if (cloud != null) {
@Nullable
RdsPlants plantClass = null;
try {
String url = URL_PLANTS;
logger.debug(LOG_HTTP_COMMAND, HTTP_GET, url.length());
logger.debug(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
String json = RdsDataPoints.httpGenericGetJson(cloud.getApiKey(), cloud.getToken(), url);
if (logger.isTraceEnabled()) {
logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK,
json.substring(0, Math.min(json.length(), 30)));
}
plantClass = RdsPlants.createFromJson(json);
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "discoverPlants()", e.getClass().getName(), e.getMessage());
return;
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "discoverPlants()", e.getClass().getName(), e.getMessage());
return;
}
if (plantClass != null) {
List<PlantInfo> plants = plantClass.getPlants();
if (plants != null) {
for (PlantInfo plant : plants) {
publishPlant(plant);
}
}
}
}
}
private void publishPlant(PlantInfo plant) {
RdsCloudHandler cloud = this.cloud;
try {
if (cloud == null) {
throw new RdsCloudException("missing cloud handler");
}
String plantId = plant.getId();
String url = String.format(URL_POINTS, plantId);
if (logger.isTraceEnabled()) {
logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
}
String json = RdsDataPoints.httpGenericGetJson(cloud.getApiKey(), cloud.getToken(), url);
if (logger.isTraceEnabled()) {
logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
}
RdsDataPoints points = RdsDataPoints.createFromJson(json);
if (points == null) {
throw new RdsCloudException("no points returned");
}
State desc = points.getPointByClass(HIE_DESCRIPTION).getState();
String label = desc.toString().replaceAll("\\s+", "_");
ThingTypeUID typeUID = THING_TYPE_RDS;
ThingUID bridgeUID = cloud.getThing().getUID();
ThingUID plantUID = new ThingUID(typeUID, bridgeUID, plantId);
DiscoveryResult disco = DiscoveryResultBuilder.create(plantUID).withBridge(bridgeUID).withLabel(label)
.withProperty(PROP_PLANT_ID, plantId).withRepresentationProperty(PROP_PLANT_ID).build();
logger.debug("discovered typeUID={}, plantUID={}, brigeUID={}, label={}, plantId={}, ", typeUID, plantUID,
bridgeUID, label, plantId);
thingDiscovered(disco);
;
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "publishPlant()", e.getClass().getName(), e.getMessage());
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "publishPlant()", e.getClass().getName(), e.getMessage());
}
}
}

View File

@@ -0,0 +1,423 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.IOException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.siemensrds.points.BasePoint;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonParseException;
/**
* The {@link RdsHandler} is the OpenHab Handler for Siemens RDS smart
* thermostats
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsHandler extends BaseThingHandler {
protected final Logger logger = LoggerFactory.getLogger(RdsHandler.class);
private @Nullable ScheduledFuture<?> lazyPollingScheduler = null;
private @Nullable ScheduledFuture<?> fastPollingScheduler = null;
private final AtomicInteger fastPollingCallsToGo = new AtomicInteger();
private RdsDebouncer debouncer = new RdsDebouncer();
private @Nullable RdsConfiguration config = null;
private @Nullable RdsDataPoints points = null;
public RdsHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command != RefreshType.REFRESH) {
doHandleCommand(channelUID.getId(), command);
}
startFastPollingBurst();
}
@Override
public void initialize() {
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
RdsConfiguration config = this.config = getConfigAs(RdsConfiguration.class);
if (config.plantId.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing Plant Id");
return;
}
updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.CONFIGURATION_PENDING);
try {
RdsCloudHandler cloud = getCloudHandler();
if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
return;
}
initializePolling();
} catch (RdsCloudException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing cloud server handler");
return;
}
}
public void initializePolling() {
try {
int pollInterval = getCloudHandler().getPollInterval();
// create a "lazy" polling scheduler
ScheduledFuture<?> lazyPollingScheduler = this.lazyPollingScheduler;
if (lazyPollingScheduler == null || lazyPollingScheduler.isCancelled()) {
this.lazyPollingScheduler = scheduler.scheduleWithFixedDelay(this::lazyPollingSchedulerExecute,
pollInterval, pollInterval, TimeUnit.SECONDS);
}
// create a "fast" polling scheduler
fastPollingCallsToGo.set(FAST_POLL_CYCLES);
ScheduledFuture<?> fastPollingScheduler = this.fastPollingScheduler;
if (fastPollingScheduler == null || fastPollingScheduler.isCancelled()) {
this.fastPollingScheduler = scheduler.scheduleWithFixedDelay(this::fastPollingSchedulerExecute,
FAST_POLL_INTERVAL, FAST_POLL_INTERVAL, TimeUnit.SECONDS);
}
startFastPollingBurst();
} catch (RdsCloudException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
logger.warn(LOG_SYSTEM_EXCEPTION, "initializePolling()", e.getClass().getName(), e.getMessage());
}
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
if (fastPollingScheduler == null) {
initializePolling();
}
}
}
@Override
public void dispose() {
// clean up the lazy polling scheduler
ScheduledFuture<?> lazyPollingScheduler = this.lazyPollingScheduler;
if (lazyPollingScheduler != null && !lazyPollingScheduler.isCancelled()) {
lazyPollingScheduler.cancel(true);
this.lazyPollingScheduler = null;
}
// clean up the fast polling scheduler
ScheduledFuture<?> fastPollingScheduler = this.fastPollingScheduler;
if (fastPollingScheduler != null && !fastPollingScheduler.isCancelled()) {
fastPollingScheduler.cancel(true);
this.fastPollingScheduler = null;
}
}
/*
* private method: initiate a burst of fast polling requests
*/
public void startFastPollingBurst() {
fastPollingCallsToGo.set(FAST_POLL_CYCLES);
}
/*
* private method: this is the callback used by the lazy polling scheduler..
* polls for the info for all points
*/
private synchronized void lazyPollingSchedulerExecute() {
doPollNow();
if (fastPollingCallsToGo.get() > 0) {
fastPollingCallsToGo.decrementAndGet();
}
}
/*
* private method: this is the callback used by the fast polling scheduler..
* checks if a fast polling burst is scheduled, and if so calls
* lazyPollingSchedulerExecute
*/
private void fastPollingSchedulerExecute() {
if (fastPollingCallsToGo.get() > 0) {
lazyPollingSchedulerExecute();
}
}
/*
* private method: send request to the cloud server for a new list of data point
* states
*/
private void doPollNow() {
try {
RdsCloudHandler cloud = getCloudHandler();
if (cloud.getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "cloud server offline");
return;
}
RdsDataPoints points = this.points;
if ((points == null || (!points.refresh(cloud.getApiKey(), cloud.getToken())))) {
points = fetchPoints();
}
if (points == null) {
if (getThing().getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "missing data points");
}
throw new RdsCloudException("missing data points");
}
if (!points.isOnline()) {
if (getThing().getStatus() == ThingStatus.ONLINE) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"cloud server reports device offline");
}
return;
}
if (getThing().getStatus() != ThingStatus.ONLINE) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, "server response ok");
}
for (ChannelMap channel : CHAN_MAP) {
if (!debouncer.timeExpired(channel.id)) {
continue;
}
BasePoint point = points.getPointByClass(channel.clazz);
State state = null;
switch (channel.id) {
case CHA_ROOM_TEMP:
case CHA_ROOM_HUMIDITY:
case CHA_OUTSIDE_TEMP:
case CHA_TARGET_TEMP: {
state = point.getState();
break;
}
case CHA_ROOM_AIR_QUALITY:
case CHA_ENERGY_SAVINGS_LEVEL: {
state = point.getEnum();
break;
}
case CHA_OUTPUT_STATE: {
state = point.getEnum();
// convert the state text "Neither" to the easier to understand word "Off"
if (STATE_NEITHER.equals(state.toString())) {
state = new StringType(STATE_OFF);
}
break;
}
case CHA_STAT_AUTO_MODE: {
state = OnOffType.from(point.getPresentPriority() > 13
|| points.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt() == 2);
break;
}
case CHA_STAT_OCC_MODE_PRESENT: {
state = OnOffType.from(point.asInt() == 3);
break;
}
case CHA_DHW_AUTO_MODE: {
state = OnOffType.from(point.getPresentPriority() > 13);
break;
}
case CHA_DHW_OUTPUT_STATE: {
state = OnOffType.from(point.asInt() == 2);
break;
}
}
if (state != null) {
updateState(channel.id, state);
}
}
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "doPollNow()", e.getClass().getName(), e.getMessage());
}
}
/*
* private method: sends a new channel value to the cloud server
*/
private synchronized void doHandleCommand(String channelId, Command command) {
RdsDataPoints points = this.points;
try {
RdsCloudHandler cloud = getCloudHandler();
String apiKey = cloud.getApiKey();
String token = cloud.getToken();
if ((points == null || (!points.refresh(apiKey, token)))) {
points = fetchPoints();
}
if (points == null) {
throw new RdsCloudException("missing data points");
}
for (ChannelMap channel : CHAN_MAP) {
if (channelId.equals(channel.id)) {
switch (channel.id) {
case CHA_TARGET_TEMP: {
Command doCommand = command;
if (command instanceof QuantityType<?>) {
Unit<?> unit = points.getPointByClass(channel.clazz).getUnit();
QuantityType<?> temp = ((QuantityType<?>) command).toUnit(unit);
if (temp != null) {
doCommand = temp;
}
}
points.setValue(apiKey, token, channel.clazz, doCommand.format("%s"));
debouncer.initialize(channelId);
break;
}
case CHA_STAT_AUTO_MODE: {
/*
* this command is particularly funky.. use Green Leaf = 5 to set to Auto, and
* use Comfort Button = 1 to set to Manual
*/
if (command == OnOffType.ON) {
points.setValue(apiKey, token, HIE_ENERGY_SAVINGS_LEVEL, "5");
} else {
points.setValue(apiKey, token, HIE_STAT_CMF_BTN, "1");
}
debouncer.initialize(channelId);
break;
}
case CHA_STAT_OCC_MODE_PRESENT: {
points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "2" : "3");
debouncer.initialize(channelId);
break;
}
case CHA_DHW_AUTO_MODE: {
if (command == OnOffType.ON) {
points.setValue(apiKey, token, channel.clazz, "0");
} else {
points.setValue(apiKey, token, channel.clazz,
Integer.toString(points.getPointByClass(channel.clazz).asInt()));
}
debouncer.initialize(channelId);
break;
}
case CHA_DHW_OUTPUT_STATE: {
points.setValue(apiKey, token, channel.clazz, command == OnOffType.OFF ? "1" : "2");
debouncer.initialize(channelId);
break;
}
case CHA_ROOM_TEMP:
case CHA_ROOM_HUMIDITY:
case CHA_OUTSIDE_TEMP:
case CHA_ROOM_AIR_QUALITY:
case CHA_OUTPUT_STATE: {
logger.debug("error: unexpected command to channel {}", channel.id);
break;
}
}
break;
}
}
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "doHandleCommand()", e.getClass().getName(), e.getMessage());
}
}
/*
* private method: returns the cloud server handler
*/
private RdsCloudHandler getCloudHandler() throws RdsCloudException {
@Nullable
Bridge b;
@Nullable
BridgeHandler h;
if ((b = getBridge()) != null && (h = b.getHandler()) != null && h instanceof RdsCloudHandler) {
return (RdsCloudHandler) h;
}
throw new RdsCloudException("no cloud handler found");
}
public @Nullable RdsDataPoints fetchPoints() {
RdsConfiguration config = this.config;
try {
if (config == null) {
throw new RdsCloudException("missing configuration");
}
String url = String.format(URL_POINTS, config.plantId);
if (logger.isTraceEnabled()) {
logger.trace(LOG_HTTP_COMMAND, HTTP_GET, url.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_SENDING_MARK, url);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_HTTP_COMMAND_ABR, HTTP_GET, url.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_SENDING_MARK, url.substring(0, Math.min(url.length(), 30)));
}
RdsCloudHandler cloud = getCloudHandler();
String apiKey = cloud.getApiKey();
String token = cloud.getToken();
String json = RdsDataPoints.httpGenericGetJson(apiKey, token, url);
if (logger.isTraceEnabled()) {
logger.trace(LOG_CONTENT_LENGTH, LOG_RECEIVED_MSG, json.length());
logger.trace(LOG_PAYLOAD_FMT, LOG_RECEIVED_MARK, json);
} else if (logger.isDebugEnabled()) {
logger.debug(LOG_CONTENT_LENGTH_ABR, LOG_RECEIVED_MSG, json.length());
logger.debug(LOG_PAYLOAD_FMT_ABR, LOG_RECEIVED_MARK, json.substring(0, Math.min(json.length(), 30)));
}
return this.points = RdsDataPoints.createFromJson(json);
} catch (RdsCloudException e) {
logger.warn(LOG_SYSTEM_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
} catch (JsonParseException | IOException e) {
logger.warn(LOG_RUNTIME_EXCEPTION, "fetchPoints()", e.getClass().getName(), e.getMessage());
}
return this.points = null;
}
}

View File

@@ -0,0 +1,124 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Component;
/**
* The {@link RdsHandlerFactory} creates things and thing handlers
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.siemensrds", service = ThingHandlerFactory.class)
public class RdsHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
.unmodifiableSet(new HashSet<>(Arrays.asList(THING_TYPE_CLOUD, THING_TYPE_RDS)));
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discos = new HashMap<>();
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if ((thingTypeUID.equals(THING_TYPE_CLOUD)) && (thing instanceof Bridge)) {
RdsCloudHandler handler = new RdsCloudHandler((Bridge) thing);
createDiscoveryService(handler);
return handler;
}
if (thingTypeUID.equals(THING_TYPE_RDS)) {
return new RdsHandler(thing);
}
return null;
}
@Override
protected synchronized void removeHandler(ThingHandler handler) {
if (handler instanceof RdsCloudHandler) {
destroyDiscoveryService((RdsCloudHandler) handler);
}
}
/*
* create a discovery service so that a newly created cloud account will find
* the things that it supports
*/
private synchronized void createDiscoveryService(RdsCloudHandler handler) {
// create a new discovery service
RdsDiscoveryService ds = new RdsDiscoveryService(handler);
// register the discovery service
ServiceRegistration<?> serviceReg = bundleContext.registerService(DiscoveryService.class.getName(), ds,
new Hashtable<>());
/*
* store service registration in a list so we can destroy it when the respective
* hub is destroyed
*/
discos.put(handler.getThing().getUID(), serviceReg);
// finally activate the discovery service
ds.activate();
}
/*
* destroy the discovery service
*/
private synchronized void destroyDiscoveryService(RdsCloudHandler handler) {
// fetch the respective thing's service registration from our list
@Nullable
ServiceRegistration<?> serviceReg = discos.remove(handler.getThing().getUID());
// retrieve the respective discovery service
if (serviceReg != null) {
RdsDiscoveryService disco = (RdsDiscoveryService) bundleContext.getService(serviceReg.getReference());
// unregister the service
serviceReg.unregister();
// deactivate the service
if (disco != null) {
disco.deactivate();
}
}
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
/**
*
* Interface to the Plants List of a particular User
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsPlants {
protected final Logger logger = LoggerFactory.getLogger(RdsPlants.class);
@SerializedName("items")
private @Nullable List<PlantInfo> plants;
private static final Gson GSON = new Gson();
@SuppressWarnings("null")
@NonNullByDefault
public static class PlantInfo {
@SerializedName("id")
private @Nullable String plantId;
@SerializedName("isOnline")
private boolean online;
public String getId() throws RdsCloudException {
String plantId = this.plantId;
if (plantId != null) {
return plantId;
}
throw new RdsCloudException("plant has no Id");
}
public boolean isOnline() {
return online;
}
}
/*
* public method: parse JSON, and create a class that encapsulates the data
*/
public static @Nullable RdsPlants createFromJson(String json) {
return GSON.fromJson(json, RdsPlants.class);
}
/*
* public method: return the plant list
*/
public @Nullable List<PlantInfo> getPlants() {
return plants;
}
}

View File

@@ -0,0 +1,195 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import com.google.gson.annotations.SerializedName;
import tec.uom.se.AbstractUnit;
import tec.uom.se.unit.Units;
/**
* private class: a generic data point
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public abstract class BasePoint {
/*
* note: temperature symbols with a degree sign: the MVN Spotless formatter
* trashes the "degree" (looks like *) symbol, so we must escape these symbols
* as octal \260 or unicode \u00B00
*/
public static final String DEGREES_CELSIUS = "\260C";
public static final String DEGREES_FAHRENHEIT = "\260F";
public static final String DEGREES_KELVIN = "K";
public static final String PERCENT_RELATIVE_HUMIDITY = "%r.H.";
public static final int UNDEFINED_VALUE = -1;
@SerializedName("rep")
protected int rep;
@SerializedName("type")
protected int type;
@SerializedName("write")
protected boolean write;
@SerializedName("descr")
protected @Nullable String descr;
@SerializedName("limits")
protected float @Nullable [] limits;
@SerializedName("descriptionName")
protected @Nullable String descriptionName;
@SerializedName("objectName")
protected @Nullable String objectName;
@SerializedName("memberName")
private @Nullable String memberName;
@SerializedName("hierarchyName")
private @Nullable String hierarchyName;
@SerializedName("translated")
protected boolean translated;
@SerializedName("presentPriority")
protected int presentPriority;
private @Nullable String @Nullable [] enumVals;
private boolean enumParsed = false;
protected boolean isEnum = false;
/*
* initialize the enum value list
*/
private boolean initEnum() {
if (!enumParsed) {
String descr = this.descr;
if (descr != null && descr.contains("*")) {
enumVals = descr.split("\\*");
isEnum = true;
}
}
enumParsed = true;
return isEnum;
}
public int getPresentPriority() {
return presentPriority;
}
/*
* abstract methods => MUST be overridden
*/
public abstract int asInt();
public void refreshValueFrom(BasePoint from) {
presentPriority = from.presentPriority;
}
protected boolean isEnum() {
return (enumParsed ? isEnum : initEnum());
}
public State getEnum() {
if (isEnum()) {
int index = asInt();
String[] enumVals = this.enumVals;
if (index >= 0 && enumVals != null && index < enumVals.length) {
return new StringType(enumVals[index]);
}
}
return UnDefType.NULL;
}
/*
* property getter for openHAB State => MUST be overridden
*/
public State getState() {
return UnDefType.NULL;
}
/*
* property getter for openHAB returns the Units of Measure of the point value
*/
public Unit<?> getUnit() {
/*
* determine the Units of Measure if available; note that other possible units
* (Ampere, hours, milliseconds, minutes) are currently not implemented
*/
String descr = this.descr;
if (descr != null) {
switch (descr) {
case DEGREES_CELSIUS: {
return SIUnits.CELSIUS;
}
case DEGREES_FAHRENHEIT: {
return ImperialUnits.FAHRENHEIT;
}
case DEGREES_KELVIN: {
return Units.KELVIN;
}
case PERCENT_RELATIVE_HUMIDITY: {
return Units.PERCENT;
}
}
}
return AbstractUnit.ONE;
}
/*
* property getter for JSON => MAY be overridden
*/
public String commandJson(String newVal) {
if (isEnum()) {
String[] enumVals = this.enumVals;
if (enumVals != null) {
for (int index = 0; index < enumVals.length; index++) {
if (enumVals[index].equals(newVal)) {
return String.format("{\"value\":%d}", index);
}
}
}
}
return String.format("{\"value\":%s}", newVal);
}
public String getMemberName() {
String memberName = this.memberName;
return memberName != null ? memberName : "undefined";
}
private @Nullable String hierarchyNameSuffix() {
String fullHierarchyName = this.hierarchyName;
if (fullHierarchyName != null) {
int suffixPosition = fullHierarchyName.lastIndexOf("'");
if (suffixPosition >= 0) {
return fullHierarchyName.substring(suffixPosition, fullHierarchyName.length());
}
}
return fullHierarchyName;
}
public String getPointClass() {
String shortHierarchyName = hierarchyNameSuffix();
if (shortHierarchyName != null) {
return shortHierarchyName;
}
return "#".concat(getMemberName());
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import com.google.gson.annotations.SerializedName;
/**
* private class a data point where "value" is a nested JSON numeric element
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class NestedNumberPoint extends BasePoint {
@SerializedName("value")
protected @Nullable NestedNumberValue inner;
@Override
public int asInt() {
NestedNumberValue inner = this.inner;
if (inner != null) {
Number innerValue = inner.value;
if (innerValue != null) {
return innerValue.intValue();
}
}
return UNDEFINED_VALUE;
}
@Override
public State getState() {
NestedNumberValue inner = this.inner;
if (inner != null) {
Number innerValue = inner.value;
if (innerValue != null) {
return new QuantityType<>(innerValue.doubleValue(), getUnit());
}
}
return UnDefType.NULL;
}
@Override
public int getPresentPriority() {
NestedNumberValue inner = this.inner;
return inner != null ? inner.presentPriority : UNDEFINED_VALUE;
}
public void setPresentPriority(int value) {
NestedNumberValue inner = this.inner;
if (inner != null) {
inner.presentPriority = value;
}
}
@Override
public void refreshValueFrom(BasePoint from) {
super.refreshValueFrom(from);
if (from instanceof NestedNumberPoint) {
NestedNumberValue fromInner = ((NestedNumberPoint) from).inner;
NestedNumberValue thisInner = this.inner;
if (thisInner != null && fromInner != null) {
thisInner.value = fromInner.value;
thisInner.presentPriority = fromInner.presentPriority;
}
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* private class inner (helper) class for an embedded JSON numeric element
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class NestedNumberValue {
@SerializedName("value")
protected @Nullable Number value;
@SerializedName("presentPriority")
protected int presentPriority;
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
import com.google.gson.annotations.SerializedName;
/**
* private class a data point where "value" is a JSON numeric element
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class NumberPoint extends BasePoint {
@SerializedName("value")
private @Nullable Number value;
@Override
public int asInt() {
Number value = this.value;
return value != null ? value.intValue() : UNDEFINED_VALUE;
}
@Override
public State getState() {
Number value = this.value;
return value != null ? new DecimalType(value.doubleValue()) : new DecimalType(UNDEFINED_VALUE);
}
@Override
public void refreshValueFrom(BasePoint from) {
super.refreshValueFrom(from);
if (from instanceof NumberPoint) {
this.value = ((NumberPoint) from).value;
}
}
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
/**
* private class a JSON de-serializer for the Data Point classes above
*
* @author Andrew Fiddian-Green - Initial contribution
*/
@NonNullByDefault
public class PointDeserializer implements JsonDeserializer<BasePoint> {
private static enum PointType {
UNDEFINED,
STRING,
NESTED_NUMBER,
NUMBER
}
@Override
public BasePoint deserialize(@Nullable JsonElement element, @Nullable Type guff,
@Nullable JsonDeserializationContext ctxt) throws JsonParseException {
if (element == null || ctxt == null) {
throw new JsonParseException("method called with null argument(s)");
}
JsonObject obj = element.getAsJsonObject();
JsonElement value = obj.get("value");
if (value == null) {
UndefPoint point = ctxt.deserialize(obj, UndefPoint.class);
if (point != null) {
return point;
}
throw new JsonSyntaxException("unable to parse point WITHOUT a \"value\" element");
}
PointType pointType = PointType.UNDEFINED;
boolean valueIsPrimitive = value.isJsonPrimitive();
JsonElement rep = obj.get("rep");
if (rep != null && rep.isJsonPrimitive() && rep.getAsJsonPrimitive().isNumber()) {
/*
* full point lists have a "rep" element so we know explicitly the point class
*/
int repValue = rep.getAsInt();
if (repValue == 0) {
pointType = PointType.STRING;
} else if (repValue < 4) {
pointType = valueIsPrimitive ? PointType.NUMBER : PointType.NESTED_NUMBER;
}
} else {
/*
* refresh point lists do NOT have a "rep" element so try to infer the point
* class
*/
if (valueIsPrimitive) {
JsonPrimitive primitiveType = value.getAsJsonPrimitive();
pointType = primitiveType.isString() ? PointType.STRING : PointType.NUMBER;
} else
pointType = PointType.NESTED_NUMBER;
}
BasePoint point;
switch (pointType) {
case STRING: {
point = ctxt.deserialize(obj, StringPoint.class);
if (point != null) {
return point;
}
break;
}
case NESTED_NUMBER: {
point = ctxt.deserialize(obj, NestedNumberPoint.class);
if (point != null) {
return point;
}
break;
}
case NUMBER: {
point = ctxt.deserialize(obj, NumberPoint.class);
if (point != null) {
return point;
}
break;
}
default: {
point = ctxt.deserialize(obj, UndefPoint.class);
if (point != null) {
return point;
}
}
}
throw new JsonSyntaxException("unable to parse point with a \"value\" element");
}
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import com.google.gson.annotations.SerializedName;
/**
* private class a data point where "value" is a JSON text element
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class StringPoint extends BasePoint {
@SerializedName("value")
private @Nullable String value;
@Override
public int asInt() {
try {
return Integer.parseInt(value);
} catch (Exception e) {
return UNDEFINED_VALUE;
}
}
@Override
public State getState() {
return new StringType(value);
}
@Override
public void refreshValueFrom(BasePoint from) {
super.refreshValueFrom(from);
if (from instanceof StringPoint) {
this.value = ((StringPoint) from).value;
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.points;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* private class a data point where "value" is unknown
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class UndefPoint extends BasePoint {
@Override
public State getState() {
return UnDefType.UNDEF;
}
@Override
public int asInt() {
return UNDEFINED_VALUE;
}
@Override
public void refreshValueFrom(BasePoint from) {
// do nothing
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="siemensrds" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Siemens RDS Binding</name>
<description>This is the binding for Siemens RDS smart thermostats</description>
<author>Andrew Fiddian-Green</author>
</binding:binding>

View File

@@ -0,0 +1,203 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="siemensrds"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="climatixic">
<label>Siemens Climatix IC Account</label>
<description>The Siemens Climatix IC cloud server account for accessing RDS Smart Thermostats</description>
<properties>
<property name="vendor">Siemens</property>
<property name="modelId">ClimatixIC</property>
</properties>
<config-description>
<parameter name="userEmail" type="text" required="true">
<label>User E-mail Address</label>
<description>The e-mail address that was used to register the smart thermostats</description>
</parameter>
<parameter name="userPassword" type="text" required="true">
<context>password</context>
<label>User Password</label>
<description>The password that was used to register the smart thermostats</description>
</parameter>
<parameter name="pollingInterval" type="integer" min="8" max="60" required="true">
<label>Polling Interval</label>
<description>Time (seconds) between polling requests (min=8, max/default=60)</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="apiKey" type="text" required="true">
<label>Climatix IC API Key</label>
<description>The key needed to access the Siemens Climatix IC cloud server</description>
</parameter>
</config-description>
</bridge-type>
<thing-type id="rds">
<supported-bridge-type-refs>
<bridge-type-ref id="climatixic"/>
</supported-bridge-type-refs>
<label>RDS Thermostat</label>
<description>Siemens RDS Smart Thermostat</description>
<channels>
<channel id="roomTemperature" typeId="temperature">
<label>Room Temperature</label>
<description>Actual room temperature</description>
</channel>
<channel id="targetTemperature" typeId="targetTemperature">
<label>Target Temperature</label>
<description>Target temperature setting for the room</description>
</channel>
<channel id="thermostatOutputState" typeId="thermostatOutputState">
<label>Thermostat Output State</label>
<description>The output state of the the thermostat (Heating, Cooling)</description>
</channel>
<channel id="roomHumidity" typeId="roomHumidity">
<label>Room Humidity</label>
<description>Actual room humidity</description>
</channel>
<channel id="roomAirQuality" typeId="roomAirQuality">
<label>Room Air Quality</label>
<description>Actual room air quality</description>
</channel>
<channel id="outsideTemperature" typeId="temperature">
<label>Outside Temperature</label>
<description>Actual outside temperature</description>
</channel>
<channel id="energySavingsLevel" typeId="energySavingsLevel">
<label>Energy Savings Level</label>
<description>Energy savings level (Green Leaf)</description>
</channel>
<channel id="thermostatAutoMode" typeId="thermostatAutoMode">
<label>Thermostat Auto Mode</label>
<description>The thermostat is in Automatic Mode (Off = Manual Mode)</description>
</channel>
<channel id="occupancyModePresent" typeId="occupancyModePresent">
<label>Occupancy Mode Present</label>
<description>The thermostat is in Present Occupancy Mode (Off = Away Mode)</description>
</channel>
<channel id="hotWaterAutoMode" typeId="hotWaterAutoMode">
<label>Hotwater Auto Mode</label>
<description>The domestic water heating is in Automatic Mode (Off = Manual Mode)</description>
</channel>
<channel id="hotWaterOutputState" typeId="hotWaterOutputState">
<label>Hotwater Output State</label>
<description>The On/Off state of the domestic water heating</description>
</channel>
</channels>
<properties>
<property name="vendor">Siemens</property>
<property name="modelId">RDS</property>
</properties>
<config-description>
<parameter name="plantId" type="text" required="true">
<label>Plant Id</label>
<description>The Plant Id of the thermostat in the Siemens Climatix IC cloud account</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Measured temperature value</description>
<category>temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="targetTemperature">
<item-type>Number:Temperature</item-type>
<label>Target Temperature</label>
<description>Target temperature setting</description>
<category>temperature</category>
<state readOnly="false" pattern="%.1f %unit%" min="5" max="30" step="0.5"/>
</channel-type>
<channel-type id="thermostatOutputState">
<item-type>String</item-type>
<label>Thermostat Output State</label>
<description>The output state of the the thermostat (Heating, Cooling)</description>
<category>fire</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="roomHumidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Measured humidity value</description>
<category>humidity</category>
<state readOnly="true" pattern="%.0f %%r.H."/>
</channel-type>
<channel-type id="roomAirQuality">
<item-type>String</item-type>
<label>Air Quality</label>
<description>Room Air Quality</description>
<category>qualityofservice</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="energySavingsLevel">
<item-type>String</item-type>
<label>Energy Savings Level</label>
<description>Energy savings level (Green Leaf)</description>
<category>qualityofservice</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="thermostatAutoMode">
<item-type>Switch</item-type>
<label>Thermostat Auto Mode</label>
<description>The thermostat is in Automatic Mode (Off = Manual Mode)</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="occupancyModePresent">
<item-type>Switch</item-type>
<label>Occupancy Mode Present</label>
<description>The thermostat is in Present Occupancy Mode (Off = Away Mode)</description>
<category>presence</category>
<state readOnly="false"/>
</channel-type>
<channel-type id="hotWaterAutoMode">
<item-type>Switch</item-type>
<label>Hotwater Auto Mode</label>
<description>The domestic water heating is in Automatic Mode (Off = Manual Mode)</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="hotWaterOutputState">
<item-type>Switch</item-type>
<label>Hotwater Output State</label>
<description>The On/Off state of the domestic water heating</description>
<state readOnly="false"/>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,517 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.siemensrds.test;
import static org.junit.Assert.*;
import static org.openhab.binding.siemensrds.internal.RdsBindingConstants.*;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.Test;
import org.openhab.binding.siemensrds.internal.RdsAccessToken;
import org.openhab.binding.siemensrds.internal.RdsCloudException;
import org.openhab.binding.siemensrds.internal.RdsDataPoints;
import org.openhab.binding.siemensrds.internal.RdsPlants;
import org.openhab.binding.siemensrds.internal.RdsPlants.PlantInfo;
import org.openhab.binding.siemensrds.points.BasePoint;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.types.State;
import tec.uom.se.unit.Units;
/**
* test suite
*
* @author Andrew Fiddian-Green - Initial contribution
*
*/
@NonNullByDefault
public class RdsTestData {
private String load(String fileName) {
try (FileReader file = new FileReader(String.format("src/test/resources/%s.json", fileName));
BufferedReader reader = new BufferedReader(file)) {
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
builder.append(line).append("\n");
}
return builder.toString();
} catch (IOException e) {
fail(e.getMessage());
}
return "";
}
@Test
public void testRdsDataPointsFullNew() {
RdsDataPoints dataPoints = RdsDataPoints.createFromJson(load("datapoints_full_set_new"));
assertNotNull(dataPoints);
try {
assertEquals("Downstairs", dataPoints.getDescription());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
@Nullable
Map<String, @Nullable BasePoint> points = dataPoints.points;
assertNotNull(points);
assertEquals(70, points.size());
}
@Test
public void confirmDegreeSymbolCodingNotTrashed() {
/*
* note: temperature symbols with a degree sign: the MVN Spotless trashes the
* "degree" (looks like *) symbol, so we must escape these symbols as octal \260
* or unicode \u00B00 - the following test will indicate is all is ok
*/
assertTrue("\260C".equals(BasePoint.DEGREES_CELSIUS));
assertTrue("\u00B0C".equals(BasePoint.DEGREES_CELSIUS));
assertTrue("\260F".equals(BasePoint.DEGREES_FAHRENHEIT));
assertTrue("\u00B0F".equals(BasePoint.DEGREES_FAHRENHEIT));
assertTrue(BasePoint.DEGREES_FAHRENHEIT.startsWith(BasePoint.DEGREES_CELSIUS.substring(0, 1)));
}
@Test
public void testRdsDataPointsRefresh() {
RdsDataPoints refreshPoints = RdsDataPoints.createFromJson(load("datapoints_refresh_set"));
assertNotNull(refreshPoints);
assertNotNull(refreshPoints.points);
Map<String, @Nullable BasePoint> refreshMap = refreshPoints.points;
assertNotNull(refreshMap);
@Nullable
BasePoint point;
State state;
// check the parsed values
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;0!Online");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), DecimalType.class);
assertEquals(1, ((DecimalType) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!00000000E000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(12.60, ((QuantityType<?>) state).floatValue(), 0.01);
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000083000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(16.0, ((QuantityType<?>) state).floatValue(), 0.01);
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000085000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(39.13, ((QuantityType<?>) state).floatValue(), 0.01);
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000086000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(21.51, ((QuantityType<?>) state).floatValue(), 0.01);
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000051000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(2, ((QuantityType<?>) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000052000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(5, ((QuantityType<?>) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000053000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(2, ((QuantityType<?>) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000056000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(1, ((QuantityType<?>) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!01300005A000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(2, ((QuantityType<?>) state).intValue());
point = refreshMap.get("Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000074000055");
assertTrue(point instanceof BasePoint);
state = point.getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(4, ((QuantityType<?>) state).intValue());
RdsDataPoints originalPoints = RdsDataPoints.createFromJson(load("datapoints_full_set"));
assertNotNull(originalPoints);
assertNotNull(originalPoints.points);
// check that the refresh point types match the originals
Map<String, @Nullable BasePoint> originalMap = originalPoints.points;
assertNotNull(originalMap);
@Nullable
BasePoint refreshPoint;
@Nullable
BasePoint originalPoint;
for (String key : refreshMap.keySet()) {
refreshPoint = refreshMap.get(key);
assertTrue(refreshPoint instanceof BasePoint);
originalPoint = originalMap.get(key);
assertTrue(originalPoint instanceof BasePoint);
assertEquals(refreshPoint.getState().getClass(), originalPoint.getState().getClass());
}
}
@Test
public void testAccessToken() {
RdsAccessToken accessToken = RdsAccessToken.createFromJson(load("access_token"));
assertNotNull(accessToken);
try {
assertEquals("this-is-not-a-valid-access_token", accessToken.getToken());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
assertTrue(accessToken.isExpired());
}
@Test
public void testRdsDataPointsFull() {
RdsDataPoints dataPoints = RdsDataPoints.createFromJson(load("datapoints_full_set"));
assertNotNull(dataPoints);
try {
assertEquals("Upstairs", dataPoints.getDescription());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
@Nullable
Map<String, @Nullable BasePoint> points = dataPoints.points;
assertNotNull(points);
assertEquals(67, points.size());
try {
assertEquals("AAS-20:SU=SiUn;APT=HvacFnct18z_A;APTV=2.003;APS=1;",
dataPoints.getPointByClass("ApplicationSoftwareVersion").getState().toString());
assertEquals("Device object", dataPoints.getPointByClass("Device Description").getState().toString());
assertEquals("FW=02.32.02.27;SVS-300.1:SBC=13.22;I",
dataPoints.getPointByClass("FirmwareRevision").getState().toString());
assertEquals("RDS110", dataPoints.getPointByClass("ModelName").getState().toString());
assertEquals(0, dataPoints.getPointByClass("SystemStatus").asInt());
assertEquals(0, dataPoints.getPointByClass("UtcOffset").asInt());
assertEquals(19, dataPoints.getPointByClass("DatabaseRevision").asInt());
assertEquals(0, dataPoints.getPointByClass("LastRestartReason").asInt());
assertEquals("MDL:ASN= RDS110;HW=0.2.0;",
dataPoints.getPointByClass("ModelInformation").getState().toString());
assertEquals(1, dataPoints.getPointByClass("Active SystemLanguge").asInt());
assertEquals(26, dataPoints.getPointByClass("TimeZone").asInt());
assertEquals("160100096D", dataPoints.getPointByClass("SerialNumber").getState().toString());
assertEquals("'10010'B", dataPoints.getPointByClass("Device Features").getState().toString());
assertEquals("Upstairs", dataPoints.getPointByClass("'Description").getState().toString());
assertEquals("192.168.1.1", dataPoints.getPointByClass("'IP gefault gateway").getState().toString());
assertEquals("255.255.255.0", dataPoints.getPointByClass("'IP subnet mask").getState().toString());
assertEquals("192.168.1.42", dataPoints.getPointByClass("'IP address").getState().toString());
assertEquals(47808, dataPoints.getPointByClass("'UDP Port").asInt());
assertEquals("'F0C77F6C1895'H", dataPoints.getPointByClass("'BACnet MAC address").getState().toString());
assertEquals("sth.connectivity.ccl-siemens.com",
dataPoints.getPointByClass("'Connection URI").getState().toString());
assertEquals("this-is-not-a-valid-activation-key",
dataPoints.getPointByClass("'Activation Key").getState().toString());
assertEquals(60, dataPoints.getPointByClass("'Reconection delay").asInt());
assertEquals(0, dataPoints.getPointByClass("#Item Updates per Minute").asInt());
assertEquals(286849, dataPoints.getPointByClass("#Item Updates Total").asInt());
assertEquals("-;en", dataPoints.getPointByClass("#Languages").getState().toString());
assertEquals(1, dataPoints.getPointByClass("#Online").asInt());
assertEquals(1473, dataPoints.getPointByClass("#Traffic Inbound per Minute").asInt());
assertEquals(178130801, dataPoints.getPointByClass("#Traffic Inbound Total").asInt());
assertEquals(616, dataPoints.getPointByClass("#Traffic Outbound per Minute").asInt());
assertEquals(60624666, dataPoints.getPointByClass("#Traffic Outbound Total").asInt());
assertEquals(0, dataPoints.getPointByClass("#Item Updates per Minute").asInt());
State state;
QuantityType<?> celsius;
state = dataPoints.getPointByClass("'TOa").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(18.55, celsius.floatValue(), 0.01);
assertEquals("0.0", dataPoints.getPointByClass("'HDevElLd").getState().toString());
state = dataPoints.getPointByClass("'SpHPcf").getState();
assertTrue(state instanceof QuantityType<?>);
QuantityType<?> fahrenheit = ((QuantityType<?>) state).toUnit(ImperialUnits.FAHRENHEIT);
assertNotNull(fahrenheit);
assertEquals(24.00, fahrenheit.floatValue(), 0.01);
state = dataPoints.getPointByClass("'SpHEco").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(16.00, celsius.floatValue(), 0.01);
state = dataPoints.getPointByClass("'SpHPrt").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(6.00, celsius.floatValue(), 0.01);
state = dataPoints.getPointByClass("'SpTR").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(24.00, celsius.floatValue(), 0.01);
state = dataPoints.getPointByClass("'SpTRShft").getState();
assertTrue(state instanceof QuantityType<?>);
QuantityType<?> kelvin = ((QuantityType<?>) state).toUnit(Units.KELVIN);
assertNotNull(kelvin);
assertEquals(0, kelvin.floatValue(), 0.01);
state = dataPoints.getPointByClass("'RHuRel").getState();
assertTrue(state instanceof QuantityType<?>);
QuantityType<?> relativeHumidity = ((QuantityType<?>) state).toUnit(Units.PERCENT);
assertNotNull(relativeHumidity);
assertEquals(46.86865, relativeHumidity.floatValue(), 0.1);
state = dataPoints.getPointByClass("'RTemp").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(23.76, celsius.floatValue(), 0.01);
state = dataPoints.getPointByClass("'SpTRMaxHCmf").getState();
assertTrue(state instanceof QuantityType<?>);
celsius = ((QuantityType<?>) state).toUnit(SIUnits.CELSIUS);
assertNotNull(celsius);
assertEquals(35.00, celsius.floatValue(), 0.01);
assertEquals("30.0", dataPoints.getPointByClass("'WarmUpGrdnt").getState().toString());
state = dataPoints.getPointByClass("'TRBltnMsvAdj").getState();
assertTrue(state instanceof QuantityType<?>);
kelvin = ((QuantityType<?>) state).toUnit(Units.KELVIN);
assertNotNull(kelvin);
assertEquals(35.0, celsius.floatValue(), 0.01);
assertEquals("0.0", dataPoints.getPointByClass("'Q22Q24ElLd").getState().toString());
assertEquals("713.0", dataPoints.getPointByClass("'RAQual").getState().toString());
assertEquals("0.0", dataPoints.getPointByClass("'TmpCmfBtn").getState().toString());
assertEquals("0.0", dataPoints.getPointByClass("'CmfBtn").getState().toString());
assertEquals("0.0", dataPoints.getPointByClass("'RPscDet").getState().toString());
assertEquals("1.0", dataPoints.getPointByClass("'EnHCtl").getState().toString());
assertEquals("0.0", dataPoints.getPointByClass("'EnRPscDet").getState().toString());
assertEquals("2.0", dataPoints.getPointByClass("'OffPrtCnf").getState().toString());
assertEquals("3.0", dataPoints.getPointByClass("'OccMod").getState().toString());
assertEquals("5.0", dataPoints.getPointByClass("'REei").getState().toString());
assertEquals("2.0", dataPoints.getPointByClass("'DhwMod").getState().toString());
assertEquals("2.0", dataPoints.getPointByClass("'HCSta").getState().toString());
assertEquals("4.0", dataPoints.getPointByClass("'PrOpModRsn").getState().toString());
assertEquals("6.0", dataPoints.getPointByClass("'HCtrSet").getState().toString());
assertEquals("2.0", dataPoints.getPointByClass("'OsscSet").getState().toString());
assertEquals("4.0", dataPoints.getPointByClass("'RAQualInd").getState().toString());
assertEquals("500.0", dataPoints.getPointByClass("'KickCyc").getState().toString());
assertEquals("180000.0", dataPoints.getPointByClass("'BoDhwTiOnMin").getState().toString());
assertEquals("180000.0", dataPoints.getPointByClass("'BoDhwTiOffMin").getState().toString());
assertEquals("UNDEF", dataPoints.getPointByClass("'ROpModSched").getState().toString());
assertEquals("UNDEF", dataPoints.getPointByClass("'DhwSched").getState().toString());
assertEquals("UNDEF", dataPoints.getPointByClass("'ROpModSched").getState().toString());
assertEquals("UNDEF", dataPoints.getPointByClass("'DhwSched").getState().toString());
assertEquals("253140.0", dataPoints.getPointByClass("'OphH").getState().toString());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
// test for a missing element
State test = null;
try {
test = dataPoints.getPointByClass("missing-element").getState();
fail("expected exception did not occur");
} catch (RdsCloudException e) {
assertEquals(null, test);
}
try {
// test the all-the-way-round lookup loop
assertNotNull(dataPoints.points);
Map<String, @Nullable BasePoint> pointsMap = dataPoints.points;
assertNotNull(pointsMap);
@Nullable
BasePoint point;
for (Entry<String, @Nullable BasePoint> entry : pointsMap.entrySet()) {
point = entry.getValue();
assertTrue(point instanceof BasePoint);
// ignore UNDEF points where all-the-way-round lookup fails
if (!"UNDEF".equals(point.getState().toString())) {
@Nullable
String x = entry.getKey();
assertNotNull(x);
String y = ((BasePoint) point).getPointClass();
String z = dataPoints.pointClassToId(y);
assertEquals(x, z);
}
}
State state = null;
// test the specific points that we use
state = dataPoints.getPointByClass(HIE_DESCRIPTION).getState();
assertEquals("Upstairs", state.toString());
state = dataPoints.getPointByClass(HIE_ROOM_TEMP).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(23.761879, ((QuantityType<?>) state).floatValue(), 0.01);
state = dataPoints.getPointByClass(HIE_OUTSIDE_TEMP).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(18.55, ((QuantityType<?>) state).floatValue(), 0.01);
state = dataPoints.getPointByClass(HIE_TARGET_TEMP).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(24, ((QuantityType<?>) state).floatValue(), 0.01);
state = dataPoints.getPointByClass(HIE_ROOM_HUMIDITY).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(46.86, ((QuantityType<?>) state).floatValue(), 0.01);
state = dataPoints.getPointByClass(HIE_ROOM_AIR_QUALITY).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Good", state.toString());
assertEquals("Good", dataPoints.getPointByClass(HIE_ROOM_AIR_QUALITY).getEnum().toString());
state = dataPoints.getPointByClass(HIE_ENERGY_SAVINGS_LEVEL).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Excellent", state.toString());
assertEquals("Excellent", dataPoints.getPointByClass(HIE_ENERGY_SAVINGS_LEVEL).getEnum().toString());
state = dataPoints.getPointByClass(HIE_OUTPUT_STATE).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Heating", state.toString());
assertEquals("Heating", dataPoints.getPointByClass(HIE_OUTPUT_STATE).getEnum().toString());
state = dataPoints.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(3, ((QuantityType<?>) state).intValue());
assertEquals(3, dataPoints.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).asInt());
state = dataPoints.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Present", state.toString());
assertEquals("Present", dataPoints.getPointByClass(HIE_STAT_OCC_MODE_PRESENT).getEnum().toString());
state = dataPoints.getPointByClass(HIE_DHW_OUTPUT_STATE).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(2, ((QuantityType<?>) state).intValue());
assertEquals(2, dataPoints.getPointByClass(HIE_DHW_OUTPUT_STATE).asInt());
state = dataPoints.getPointByClass(HIE_DHW_OUTPUT_STATE).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("On", state.toString());
assertEquals("On", dataPoints.getPointByClass(HIE_DHW_OUTPUT_STATE).getEnum().toString());
state = dataPoints.getPointByClass(HIE_PR_OP_MOD_RSN).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(4, ((QuantityType<?>) state).intValue());
assertEquals(4, dataPoints.getPointByClass(HIE_PR_OP_MOD_RSN).asInt());
state = dataPoints.getPointByClass(HIE_PR_OP_MOD_RSN).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Comfort", state.toString());
assertEquals("Comfort", dataPoints.getPointByClass(HIE_PR_OP_MOD_RSN).getEnum().toString());
state = dataPoints.getPointByClass(HIE_STAT_CMF_BTN).getState();
assertEquals(state.getClass(), QuantityType.class);
assertEquals(0, ((QuantityType<?>) state).intValue());
assertEquals(0, dataPoints.getPointByClass(HIE_STAT_CMF_BTN).asInt());
state = dataPoints.getPointByClass(HIE_STAT_CMF_BTN).getEnum();
assertEquals(state.getClass(), StringType.class);
assertEquals("Inactive", state.toString());
assertEquals("Inactive", dataPoints.getPointByClass(HIE_STAT_CMF_BTN).getEnum().toString());
// test online code
assertTrue(dataPoints.isOnline());
// test present priority code
assertEquals(15, dataPoints.getPointByClass(HIE_TARGET_TEMP).getPresentPriority());
// test temperature units code (C)
BasePoint tempPoint = dataPoints.getPointByClass("'SpTR");
assertTrue(tempPoint instanceof BasePoint);
assertEquals(SIUnits.CELSIUS, ((BasePoint) tempPoint).getUnit());
// test temperature units code (F)
tempPoint = dataPoints.getPointByClass("'SpHPcf");
assertTrue(tempPoint instanceof BasePoint);
assertEquals(ImperialUnits.FAHRENHEIT, ((BasePoint) tempPoint).getUnit());
// test temperature units code (K)
tempPoint = dataPoints.getPointByClass("'SpHPcf");
assertTrue(tempPoint instanceof BasePoint);
assertEquals(ImperialUnits.FAHRENHEIT, ((BasePoint) tempPoint).getUnit());
tempPoint = dataPoints.getPointByClass("'SpTRShft");
assertTrue(tempPoint instanceof BasePoint);
assertEquals(Units.KELVIN, ((BasePoint) tempPoint).getUnit());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
}
@Test
public void testRdsPlants() {
try {
RdsPlants plants = RdsPlants.createFromJson(load("plants"));
assertNotNull(plants);
@Nullable
List<PlantInfo> plantList = plants.getPlants();
assertNotNull(plantList);
@Nullable
PlantInfo plant;
plant = plantList.get(0);
assertTrue(plant instanceof PlantInfo);
assertEquals("Pd1774247-7de7-4896-ac76-b7e0dd943c40", ((PlantInfo) plant).getId());
assertTrue(plant.isOnline());
plant = plantList.get(1);
assertTrue(plant instanceof PlantInfo);
assertEquals("Pfaf770c8-abeb-4742-ad65-ead39030d369", ((PlantInfo) plant).getId());
assertTrue(((PlantInfo) plant).isOnline());
} catch (RdsCloudException e) {
fail(e.getMessage());
}
}
}

View File

@@ -0,0 +1,8 @@
{
"access_token": "this-is-not-a-valid-access_token",
"token_type": "bearer",
"expires_in": 1209599,
"userName": "software@whitebear.ch",
".issued": "Thu, 06 Jun 2019 10:27:50 GMT",
".expires": "Thu, 20 Jun 2019 10:27:50 GMT"
}

View File

@@ -0,0 +1,101 @@
{
"totalCount": 11,
"values": {
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;0!Online": {
"value": 1
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!00000000E000055": {
"value": {
"value": 12.6014862,
"statusFlags": 0,
"reliability": 0,
"eventState": 0,
"minValue": -50,
"maxValue": 80
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000083000055": {
"value": {
"value": 16,
"statusFlags": 0,
"reliability": 0,
"presentPriority": 15,
"eventState": 0,
"minValue": 6,
"maxValue": 35
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000085000055": {
"value": {
"value": 39.1304474,
"statusFlags": 0,
"reliability": 0,
"eventState": 0,
"minValue": 0,
"maxValue": 100
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!002000086000055": {
"value": {
"value": 21.51872,
"statusFlags": 0,
"reliability": 0,
"eventState": 0,
"minValue": 0,
"maxValue": 50
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000051000055": {
"value": {
"value": 2,
"statusFlags": 0,
"reliability": 0,
"presentPriority": 13,
"eventState": 0
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000052000055": {
"value": {
"value": 5,
"statusFlags": 0,
"reliability": 0,
"presentPriority": 15,
"eventState": 0
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000053000055": {
"value": {
"value": 2,
"statusFlags": 0,
"reliability": 0,
"presentPriority": 15,
"eventState": 0
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000056000055": {
"value": {
"value": 1,
"statusFlags": 0,
"reliability": 0,
"eventState": 0
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!01300005A000055": {
"value": {
"value": 2,
"statusFlags": 0,
"reliability": 0,
"presentPriority": 13,
"eventState": 0
}
},
"Pd1774247-7de7-4896-ac76-b7e0dd943c40;1!013000074000055": {
"value": {
"value": 4,
"statusFlags": 0,
"reliability": 0,
"eventState": 0
}
}
}
}

View File

@@ -0,0 +1,61 @@
{
"totalCount": 2,
"items": [
{
"id": "Pd1774247-7de7-4896-ac76-b7e0dd943c40",
"activationKey": "this-is-not-a-valid-activation-key",
"address": "",
"alarmStatus": 0,
"applicationSetDescription": "Siemens Smart Thermostat\r\nRDS110 => Device ID 45\r\n",
"applicationSetId": "9964755b-6766-40bd-ba45-77b2446b71bb",
"applicationSetName": "STH-Default-RDS110",
"asn": "RDS110",
"assigned": true,
"city": "",
"country": "",
"description": "",
"energyIndicator": 0,
"isOnline": true,
"name": "this-is-not-a-valid-activation-key-RDS110",
"phone": "",
"serialNumber": "this-is-not-a-valid-activation-key",
"state": "",
"taskStatus": 0,
"tenant": "Siemens STH",
"tenantId": "T290ea1c1-902c-4c0b-9dce-f96119bc7fc1",
"timezone": "",
"zipCode": "",
"imsi": "",
"customerPlantId": null,
"enhancedPrivileges": false
},
{
"id": "Pfaf770c8-abeb-4742-ad65-ead39030d369",
"activationKey": "this-is-not-a-valid-activation-key",
"address": "",
"alarmStatus": 0,
"applicationSetDescription": "Siemens Smart Thermostat\r\nRDS110 => Device ID 45\r\n",
"applicationSetId": "9964755b-6766-40bd-ba45-77b2446b71bb",
"applicationSetName": "STH-Default-RDS110",
"asn": "RDS110",
"assigned": true,
"city": "",
"country": "",
"description": "",
"energyIndicator": 0,
"isOnline": true,
"name": "this-is-not-a-valid-activation-key-RDS110",
"phone": "",
"serialNumber": "this-is-not-a-valid-activation-key",
"state": "",
"taskStatus": 0,
"tenant": "Siemens STH",
"tenantId": "T290ea1c1-902c-4c0b-9dce-f96119bc7fc1",
"timezone": "",
"zipCode": "",
"imsi": "",
"customerPlantId": null,
"enhancedPrivileges": false
}
]
}