added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
38
bundles/org.openhab.binding.siemensrds/.classpath
Normal file
38
bundles/org.openhab.binding.siemensrds/.classpath
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.siemensrds/.project
Normal file
23
bundles/org.openhab.binding.siemensrds/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.siemensrds</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
13
bundles/org.openhab.binding.siemensrds/NOTICE
Normal file
13
bundles/org.openhab.binding.siemensrds/NOTICE
Normal file
@@ -0,0 +1,13 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab-addons
|
||||
128
bundles/org.openhab.binding.siemensrds/README.md
Normal file
128
bundles/org.openhab.binding.siemensrds/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Siemens RDS Binding
|
||||
|
||||
The Siemens RDS binding provides the infrastructure for connecting openHAB to the Siemens Climatix IC cloud server and integrate connected [Siemens RDS Smart thermostats](https://new.siemens.com/global/en/products/buildings/hvac/room-thermostats/smart-thermostat.html) onto the openHAB bus.
|
||||
|
||||

|
||||
|
||||
## Supported Things
|
||||
|
||||
The binding supports two types of Thing as follows..
|
||||
|
||||
| Thing Type | Description |
|
||||
|----------------------|--------------------------------------------------------------------------------------------------------------------------|
|
||||
| Climatix IC Account | User account on the Siemens Climatix IC cloud server (bridge) to connect with respective Smart Thermostat Things below.. |
|
||||
| RDS Smart Thermostat | Siemens RDS model Smart Thermostat devices |
|
||||
|
||||
## Discovery
|
||||
|
||||
You have to manually create a single (Bridge) Thing for the Climatix IC Account, and enter the required Configuration Parameters (see Thing Configuration for Climatix IC Account below).
|
||||
If the Configuration Parameters are all valid, then the Climatix IC Account Thing will automatically attempt to connect and sign on to the Siemens Climatix IC cloud server.
|
||||
If the sign on succeeds, the Thing will indicate its status as Online, otherwise it will show an error status.
|
||||
|
||||
Once the Thing of the type Climatix IC Account has been created and successfully signed on to the cloud server, it will automatically interrogate the server to discover all the respective RDS Smart Thermostat Things associated with that account.
|
||||
After a short while, all discovered RDS Smart Thermostat Things will be displayed in the Paper UI Inbox.
|
||||
If in future you add new RDS Smart Thermostat devices to your Siemens account (e.g. via the Siemens App) then these new devices will also appear in the Inbox.
|
||||
|
||||
## Thing Configuration for "Climatix IC Account"
|
||||
|
||||
The Climatix IC Account connects to the Siemens Climatix IC cloud server (bridge) to communicate with any respective RDS Smart Thermostats associated with that account.
|
||||
It signs on to the cloud server using the supplied user's credentials, and it polls the server at regular intervals to read and write the data for each Smart Thermostat that is configured in that account.
|
||||
Before it can connect to the server, the following Configuration Parameters must be entered.
|
||||
|
||||
| Configuration Parameter | Description
|
||||
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| userEmail | The e-mail address of the user account on the cloud server; as entered in the Siemens App when first registering a thermostat. |
|
||||
| userPassword | The password of the user account on the cloud server; as entered in the Siemens App. |
|
||||
| pollingInterval | Time interval in seconds between polling requests to the cloud server; the value must be between 8..60 seconds; the Default value (recommended) is 60 seconds. |
|
||||
| apiKey | The key code needed to access the application program interface on the Siemens Climatix IC cloud server; you can request a key code from Siemens on their web-site. |
|
||||
|
||||
Note: You must create ONLY ONE Thing of the type Climatix IC Account; duplicate Climatix IC Account Things risk causing communication errors with the cloud server.
|
||||
|
||||
## Thing Configuration for "RDS Smart Thermostat"
|
||||
|
||||
Each RDS Smart Thermostat Thing is identified in the Climatix IC Account by means of a unique Plant Id code.
|
||||
The Paper UI automatic discovery process determines the Plant Id codes of all connected thermostats automatically.
|
||||
|
||||
| Configuration Parameter | Description |
|
||||
|-------------------------|-------------------------------------------------------------------------------------------------------------|
|
||||
| plantId | The unique code to identify a specific RDS Smart Thermostat Thing on the Siemens Climatix IC cloud server. |
|
||||
|
||||
## Channels for RDS Smart Thermostat
|
||||
|
||||
The RDS Smart Thermostat supports several channels as shown below.
|
||||
|
||||
| Channel | Data Type | Description |
|
||||
|--------------------------|--------------------|-----------------------------------------------------------------------------|
|
||||
| roomTemperature | Number:Temperature | Actual Room Temperature |
|
||||
| targetTemperature | Number:Temperature | Target temperature setting for the room |
|
||||
| thermostatOutputState | String | The output state of the thermostat (Heating, Off, Cooling) |
|
||||
| roomHumidity | Number:Dimensionless| Actual Room Humidity |
|
||||
| roomAirQuality | String | Actual Room Air Quality (Poor..Good) |
|
||||
| outsideTemperature | Number:Temperature | Actual Outside temperature |
|
||||
| energySavingsLevel | String | Energy saving level (Green Leaf score) (Poor..Excellent) |
|
||||
| occupancyModePresent | Switch | The Thermostat is in the Present Occupancy Mode (Off=Absent, On=Present) |
|
||||
| thermostatAutoMode | Switch | The Thermostat is in Automatic Mode (Off=Manual, On=Automatic) |
|
||||
| hotWaterAutoMode | Switch | The Domestic Water Heating is in Automatic Mode (Off=Manual, On=Automatic) |
|
||||
| hotWaterOutputState | Switch | The On/Off state of the domestic water heating |
|
||||
|
||||
## Full Example
|
||||
|
||||
### `demo.things` File
|
||||
|
||||
```
|
||||
Bridge siemensrds:climatixic:mybridgename "Climatix IC Account" [ userEmail="email@example.com", userPassword="secret", apiKey="32-character-code-provided-by-siemens", pollingInterval=60 ]
|
||||
}
|
||||
```
|
||||
|
||||
To manually configure an RDS Smart Thermostat Thing requires knowledge of the "Plant Id" which is a unique code used to identify a specific thermostat device in the Siemens Climatix IC cloud server account.
|
||||
The Paper UI automatic Discovery service (see above) discovers the "Plant Id" codes during the discovery process.
|
||||
|
||||
```
|
||||
Bridge siemensrds:climatixic:mybridgename "Climatix IC Account" [ userEmail="email@example.com", userPassword="secret", apiKey="32-character-code-provided-by-siemens", pollingInterval=60 ] {
|
||||
Thing rds mydownstairs "Downstairs Thermostat" @ "Hall" [ plantId="Pd0123456-789a-bcde-0123456789abcdef0" ]
|
||||
Thing rds myupstairs "Upstairs Thermostat" @ "Landing" [ plantId="Pd0123456-789a-bcde-f0123456789abcdef" ]
|
||||
}
|
||||
```
|
||||
|
||||
### `demo.items` File
|
||||
|
||||
```
|
||||
Number:Temperature Upstairs_RoomTemperature "Room Temperature" { channel="siemensrds:rds:mybridgename:myupstairs:roomTemperature" }
|
||||
Number:Temperature Upstairs_TargetTemperature "Target Temperature" { channel="siemensrds:rds:mybridgename:myupstairs:targetTemperature" }
|
||||
String Upstairs_ThermostatOutputState "Thermostat Output State" { channel="siemensrds:rds:mybridgename:myupstairs:thermostatOutputState" }
|
||||
Number:Dimensionless Upstairs_RoomHumidity "Room Humidity" { channel="siemensrds:rds:mybridgename:myupstairs:roomHumidity" }
|
||||
String Upstairs_RoomAirQuality "Room Air Quality" { channel="siemensrds:rds:mybridgename:myupstairs:roomAirQuality" }
|
||||
Number:Temperature Upstairs_OutsideTemperature "Outside Temperature" { channel="siemensrds:rds:mybridgename:myupstairs:outsideTemperature" }
|
||||
String Upstairs_EnergySavingsLevel "Energy Savings Level" { channel="siemensrds:rds:mybridgename:myupstairs:energySavingsLevel" }
|
||||
Switch Upstairs_OccupancModePresent "Occupancy Mode Present" { channel="siemensrds:rds:mybridgename:myupstairs:occupancyModePresent" }
|
||||
Switch Upstairs_ThermostatAutoMode "Thermostat Auto Mode" { channel="siemensrds:rds:mybridgename:myupstairs:thermostatAutoMode" }
|
||||
Switch Upstairs_HotWaterAutoMode "Hotwater Auto Mode" { channel="siemensrds:rds:mybridgename:myupstairs:hotWaterAutoMode" }
|
||||
Switch Upstairs_HotWaterOutputState "Hotwater Output State" { channel="siemensrds:rds:mybridgename:myupstairs:hotWaterOutputState" }
|
||||
```
|
||||
|
||||
### `demo.sitemap` File
|
||||
|
||||
```
|
||||
sitemap siemensrds label="Siemens RDS"
|
||||
{
|
||||
Frame label="Heating" {
|
||||
Text item=Upstairs_RoomTemperature
|
||||
Setpoint item=Upstairs_TargetTemperature minValue=5 maxValue=30 step=0.5
|
||||
Switch item=Upstairs_ThermostatAutoMode
|
||||
Switch item=Upstairs_OccupancyModePresent
|
||||
Text item=Upstairs_ThermostatOutputState
|
||||
}
|
||||
|
||||
Frame label="Environment" {
|
||||
Text item=Upstairs_RoomHumidity
|
||||
Text item=Upstairs_OutsideTemperature
|
||||
Text item=Upstairs_RoomAirQuality
|
||||
Text item=Upstairs_EnergySavingsLevel
|
||||
}
|
||||
|
||||
Frame label="Hot Water" {
|
||||
Switch item=Upstairs_HotwaterAutoMode
|
||||
Switch item=Upstairs_HotwaterOutputState
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
bundles/org.openhab.binding.siemensrds/doc/rds110-family.jpg
Normal file
BIN
bundles/org.openhab.binding.siemensrds/doc/rds110-family.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
16
bundles/org.openhab.binding.siemensrds/pom.xml
Normal file
16
bundles/org.openhab.binding.siemensrds/pom.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.siemensrds</artifactId>
|
||||
<name>openHAB Add-ons :: Bundles :: Siemens RDS Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -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