added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 = "";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user