[evohome] Add null annotation and minor refactoring (#13885)

Signed-off-by: Leo Siepel <leosiepel@gmail.com>
This commit is contained in:
lsiepel 2023-02-24 16:08:53 +01:00 committed by GitHub
parent cb31f420ff
commit dd21d92a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 312 additions and 201 deletions

View File

@ -64,11 +64,12 @@ None
### Zone
| Channel Type ID | Item Type | Description |
|-------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Temperature | Number | Allows for viewing the current actual temperature of the zone. |
| SetPointStatus | String | Allows for viewing the current set point mode of the zone. |
| SetPoint | Number | Allows for viewing and permanently overriding the temperature set point of the zone. Sending 0 cancels any active set point overrides. |
| Channel Type ID | Item Type | Description |
|-----------------|--------------------|----------------------------------------------------------------------------------------------------------------------------------------|
| Temperature | Number:Temperature | Allows for viewing the current actual temperature of the zone. |
| SetPointStatus | String | Allows for viewing the current set point mode of the zone. |
| SetPoint | Number:Temperature | Allows for viewing and permanently overriding the temperature set point of the zone. Sending 0 cancels any active set point overrides. |
|
## Full Example
@ -89,9 +90,9 @@ Bridge evohome:account:your_account_alias [ username="your_user_name", password=
String DemoMode { channel="evohome:display:your_account_alias:your_display_alias:SystemMode" }
// evohome Heatingzone
Number DemoZoneTemperature { channel="evohome:heatingzone:your_account_alias:your_zone_alias:Temperature" }
String DemoZoneSetPointStatus { channel="evohome:heatingzone:your_account_alias:your_zone_alias:SetPointStatus" }
Number DemoZoneSetPoint { channel="evohome:heatingzone:your_account_alias:your_zone_alias:SetPoint" }
Number:Temperature DemoZoneTemperature { channel="evohome:heatingzone:your_account_alias:your_zone_alias:Temperature" }
String DemoZoneSetPointStatus { channel="evohome:heatingzone:your_account_alias:your_zone_alias:SetPointStatus" }
Number:Temperature DemoZoneSetPoint { channel="evohome:heatingzone:your_account_alias:your_zone_alias:SetPoint" }
```
### demo.sitemap

View File

@ -17,6 +17,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
@ -26,6 +27,7 @@ import org.openhab.core.thing.ThingTypeUID;
* @author Jasper van Zuijlen - Initial contribution
* @author Neil Renaud - Heating Zones
*/
@NonNullByDefault
public class EvohomeBindingConstants {
private static final String BINDING_ID = "evohome";

View File

@ -14,12 +14,15 @@ package org.openhab.binding.evohome.internal;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Provides an interface for a delegate that can throw a timeout
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public interface RunnableWithTimeout {
public abstract void run() throws TimeoutException;

View File

@ -18,13 +18,15 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.Authentication;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Authentication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,17 +39,17 @@ import com.google.gson.GsonBuilder;
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class ApiAccess {
private static final int REQUEST_TIMEOUT_SECONDS = 5;
private final Logger logger = LoggerFactory.getLogger(ApiAccess.class);
private final HttpClient httpClient;
private final Gson gson;
private final Gson gson = new GsonBuilder().create();
private Authentication authenticationData;
private String applicationId;
private @Nullable Authentication authenticationData;
private @Nullable String applicationId;
public ApiAccess(HttpClient httpClient) {
this.gson = new GsonBuilder().create();
this.httpClient = httpClient;
}
@ -56,7 +58,7 @@ public class ApiAccess {
*
* @param authentication The authentication details to apply
*/
public void setAuthentication(Authentication authentication) {
public void setAuthentication(@Nullable Authentication authentication) {
authenticationData = authentication;
}
@ -65,7 +67,7 @@ public class ApiAccess {
*
* @return The current authentication details
*/
public Authentication getAuthentication() {
public @Nullable Authentication getAuthentication() {
return authenticationData;
}
@ -74,7 +76,7 @@ public class ApiAccess {
*
* @param applicationId The application id to apply
*/
public void setApplicationId(String applicationId) {
public void setApplicationId(@Nullable String applicationId) {
this.applicationId = applicationId;
}
@ -89,18 +91,16 @@ public class ApiAccess {
* @return The result of the request or null
* @throws TimeoutException Thrown when a request times out
*/
public <TOut> TOut doRequest(HttpMethod method, String url, Map<String, String> headers, String requestData,
String contentType, Class<TOut> outClass) throws TimeoutException {
TOut retVal = null;
public @Nullable <TOut> TOut doRequest(HttpMethod method, String url, Map<String, String> headers,
@Nullable String requestData, String contentType, @Nullable Class<TOut> outClass) throws TimeoutException {
logger.debug("Requesting: [{}]", url);
@Nullable
TOut retVal = null;
try {
Request request = httpClient.newRequest(url).method(method);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
request.header(header.getKey(), header.getValue());
}
for (Map.Entry<String, String> header : headers.entrySet()) {
request.header(header.getKey(), header.getValue());
}
if (requestData != null) {
@ -109,8 +109,8 @@ public class ApiAccess {
ContentResponse response = request.timeout(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
logger.debug("Response: {}", response);
logger.debug("\n{}\n{}", response.getHeaders(), response.getContentAsString());
logger.trace("Response: {}", response);
logger.trace("\n{}\n{}", response.getHeaders(), response.getContentAsString());
if ((response.getStatus() == HttpStatus.OK_200) || (response.getStatus() == HttpStatus.ACCEPTED_202)) {
String reply = response.getContentAsString();
@ -118,6 +118,10 @@ public class ApiAccess {
if (outClass != null) {
retVal = new Gson().fromJson(reply, outClass);
}
} else if ((response.getStatus() == HttpStatus.CREATED_201)) {
// success nothing to return ignore
} else {
logger.debug("Request failed with unexpected response code {}", response.getStatus());
}
} catch (ExecutionException e) {
logger.debug("Error in handling request: ", e);
@ -125,7 +129,6 @@ public class ApiAccess {
logger.debug("Handling request interrupted: ", e);
Thread.currentThread().interrupt();
}
return retVal;
}
@ -138,7 +141,7 @@ public class ApiAccess {
* @return The result of the request or null
* @throws TimeoutException Thrown when a request times out
*/
public <TOut> TOut doAuthenticatedGet(String url, Class<TOut> outClass) throws TimeoutException {
public @Nullable <TOut> TOut doAuthenticatedGet(String url, Class<TOut> outClass) throws TimeoutException {
return doAuthenticatedRequest(HttpMethod.GET, url, null, outClass);
}
@ -161,16 +164,16 @@ public class ApiAccess {
* @param method The HTTP method to use (POST, GET, ...)
* @param url The URL to query
* @param headers The optional additional headers to apply, can be null
* @param requestContainer The object to use as JSON data for the request
* @param outClass The type of the requested result
* @param requestContainer The object to use as JSON data for the request, can be null
* @param outClass The type of the requested result, can be null
* @return The result of the request or null
* @throws TimeoutException Thrown when a request times out
*/
private <TOut> TOut doRequest(HttpMethod method, String url, Map<String, String> headers, Object requestContainer,
Class<TOut> outClass) throws TimeoutException {
private @Nullable <TOut> TOut doRequest(HttpMethod method, String url, Map<String, String> headers,
@Nullable Object requestContainer, @Nullable Class<TOut> outClass) throws TimeoutException {
String json = null;
if (requestContainer != null) {
json = this.gson.toJson(requestContainer);
json = gson.toJson(requestContainer);
}
return doRequest(method, url, headers, json, "application/json", outClass);
@ -188,17 +191,19 @@ public class ApiAccess {
* @return The result of the request or null
* @throws TimeoutException Thrown when a request times out
*/
private <TOut> TOut doAuthenticatedRequest(HttpMethod method, String url, Object requestContainer,
Class<TOut> outClass) throws TimeoutException {
Map<String, String> headers = null;
if (authenticationData != null) {
headers = new HashMap<>();
private @Nullable <TOut> TOut doAuthenticatedRequest(HttpMethod method, String url,
@Nullable Object requestContainer, @Nullable Class<TOut> outClass) throws TimeoutException {
Map<String, String> headers = new HashMap<>();
Authentication localAuthenticationData = authenticationData;
String localApplicationId = applicationId;
headers.put("Authorization", "Bearer " + authenticationData.getAccessToken());
headers.put("applicationId", applicationId);
headers.put("Accept",
"application/json, application/xml, text/json, text/x-json, text/javascript, text/xml");
if (localAuthenticationData != null) {
headers.put("Authorization", "Bearer " + localAuthenticationData.getAccessToken());
}
if (localApplicationId != null) {
headers.put("applicationId", localApplicationId);
}
headers.put("Accept", "application/json, application/xml, text/json, text/x-json, text/javascript, text/xml");
return doRequest(method, url, headers, requestContainer, outClass);
}

View File

@ -19,18 +19,20 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.evohome.internal.api.models.v2.request.HeatSetPoint;
import org.openhab.binding.evohome.internal.api.models.v2.request.HeatSetPointBuilder;
import org.openhab.binding.evohome.internal.api.models.v2.request.Mode;
import org.openhab.binding.evohome.internal.api.models.v2.request.ModeBuilder;
import org.openhab.binding.evohome.internal.api.models.v2.response.Authentication;
import org.openhab.binding.evohome.internal.api.models.v2.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.response.LocationStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.Locations;
import org.openhab.binding.evohome.internal.api.models.v2.response.LocationsStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.UserAccount;
import org.openhab.binding.evohome.internal.api.models.v2.dto.request.HeatSetPoint;
import org.openhab.binding.evohome.internal.api.models.v2.dto.request.HeatSetPointBuilder;
import org.openhab.binding.evohome.internal.api.models.v2.dto.request.Mode;
import org.openhab.binding.evohome.internal.api.models.v2.dto.request.ModeBuilder;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Authentication;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationsStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.UserAccount;
import org.openhab.binding.evohome.internal.configuration.EvohomeAccountConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -41,6 +43,7 @@ import org.slf4j.LoggerFactory;
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeApiClient {
private static final String APPLICATION_ID = "b013aa26-9724-4dbd-8897-048b9aada249";
@ -52,8 +55,8 @@ public class EvohomeApiClient {
private final ApiAccess apiAccess;
private Locations locations = new Locations();
private UserAccount useraccount;
private LocationsStatus locationsStatus;
private @Nullable UserAccount useraccount;
private @Nullable LocationsStatus locationsStatus;
/**
* Creates a new API client based on the V2 API interface
@ -72,7 +75,7 @@ public class EvohomeApiClient {
public void close() {
apiAccess.setAuthentication(null);
useraccount = null;
locations = null;
locations = new Locations();
locationsStatus = null;
}
@ -113,7 +116,7 @@ public class EvohomeApiClient {
return locations;
}
public LocationsStatus getInstallationStatus() {
public @Nullable LocationsStatus getInstallationStatus() {
return locationsStatus;
}
@ -139,33 +142,33 @@ public class EvohomeApiClient {
apiAccess.doAuthenticatedPut(url, heatSetPoint);
}
private UserAccount requestUserAccount() throws TimeoutException {
private @Nullable UserAccount requestUserAccount() throws TimeoutException {
String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_ACCOUNT;
return apiAccess.doAuthenticatedGet(url, UserAccount.class);
}
private Locations requestLocations() throws TimeoutException {
Locations locations = new Locations();
if (useraccount != null) {
Locations locations = null;
UserAccount localAccount = useraccount;
if (localAccount != null) {
String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_INSTALLATION_INFO;
url = String.format(url, useraccount.getUserId());
url = String.format(url, localAccount.getUserId());
locations = apiAccess.doAuthenticatedGet(url, Locations.class);
}
return locations;
return locations != null ? locations : new Locations();
}
private LocationsStatus requestLocationsStatus() throws TimeoutException {
LocationsStatus locationsStatus = new LocationsStatus();
if (locations != null) {
for (Location location : locations) {
String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_LOCATION_STATUS;
url = String.format(url, location.getLocationInfo().getLocationId());
LocationStatus status = apiAccess.doAuthenticatedGet(url, LocationStatus.class);
locationsStatus.add(status);
}
for (Location location : locations) {
String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_LOCATION_STATUS;
url = String.format(url, location.getLocationInfo().getLocationId());
LocationStatus status = apiAccess.doAuthenticatedGet(url, LocationStatus.class);
locationsStatus.add(status);
}
return locationsStatus;
}

View File

@ -12,13 +12,17 @@
*/
package org.openhab.binding.evohome.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Exception for errors from the API Client.
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeApiClientException extends Exception {
private static final long serialVersionUID = 1L;
public EvohomeApiClientException() {
}

View File

@ -12,12 +12,15 @@
*/
package org.openhab.binding.evohome.internal.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* List of evohome API constants
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeApiConstants {
public static final String URL_V2_AUTH = "https://tccna.honeywell.com/Auth/OAuth/Token";

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
import com.google.gson.annotations.SerializedName;
@ -43,12 +43,15 @@ public class HeatSetPoint {
timeUntil = null;
}
@SuppressWarnings("unused")
@SerializedName("heatSetpointValue")
private double heatSetpointValue;
@SuppressWarnings("unused")
@SerializedName("setpointMode")
private String setpointMode;
@SuppressWarnings("unused")
@SerializedName("timeUntil")
private String timeUntil;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
/**
* Builder for heat set point API requests

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
import com.google.gson.annotations.SerializedName;
@ -34,12 +34,15 @@ public class Mode {
permanent = false;
}
@SuppressWarnings("unused")
@SerializedName("systemMode")
private String systemMode;
@SuppressWarnings("unused")
@SerializedName("timeUntil")
private String timeUntil;
@SuppressWarnings("unused")
@SerializedName("permanent")
private boolean permanent;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
/**
* Builder for mode API requests

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
/**
* Builder for API requests

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.request;
package org.openhab.binding.evohome.internal.api.models.v2.dto.request;
/**
* Builder for timed API requests

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.ArrayList;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.ArrayList;
@ -21,5 +21,5 @@ import java.util.ArrayList;
*
*/
public class Locations extends ArrayList<Location> {
private static final long serialVersionUID = 1L;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.ArrayList;
@ -21,5 +21,5 @@ import java.util.ArrayList;
*
*/
public class LocationsStatus extends ArrayList<LocationStatus> {
private static final long serialVersionUID = 1L;
}

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import com.google.gson.annotations.SerializedName;

View File

@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.evohome.internal.api.models.v2.response;
package org.openhab.binding.evohome.internal.api.models.v2.dto.response;
import java.util.List;

View File

@ -12,15 +12,18 @@
*/
package org.openhab.binding.evohome.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains the configuration of the binding.
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeAccountConfiguration {
public String username;
public String password;
public String applicationId;
public String username = "";
public String password = "";
public String applicationId = "";
public int refreshInterval;
}

View File

@ -12,13 +12,16 @@
*/
package org.openhab.binding.evohome.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains the configuration of the binding.
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeTemperatureControlSystemConfiguration {
public String id;
public String name;
public String id = "";
public String name = "";
}

View File

@ -12,13 +12,16 @@
*/
package org.openhab.binding.evohome.internal.configuration;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Contains the common configuration definition of an evohome Thing
*
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeThingConfiguration {
public String id;
public String name;
public String id = "";
public String name = "";
}

View File

@ -15,11 +15,13 @@ package org.openhab.binding.evohome.internal.discovery;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.evohome.internal.EvohomeBindingConstants;
import org.openhab.binding.evohome.internal.api.models.v2.response.Gateway;
import org.openhab.binding.evohome.internal.api.models.v2.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.response.TemperatureControlSystem;
import org.openhab.binding.evohome.internal.api.models.v2.response.Zone;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Gateway;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystem;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Zone;
import org.openhab.binding.evohome.internal.handler.AccountStatusListener;
import org.openhab.binding.evohome.internal.handler.EvohomeAccountBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
@ -37,6 +39,7 @@ import org.slf4j.LoggerFactory;
* @author Jasper van Zuijlen - Background discovery
*
*/
@NonNullByDefault
public class EvohomeDiscoveryService extends AbstractDiscoveryService implements AccountStatusListener {
private final Logger logger = LoggerFactory.getLogger(EvohomeDiscoveryService.class);
private static final int TIMEOUT = 5;
@ -86,18 +89,29 @@ public class EvohomeDiscoveryService extends AbstractDiscoveryService implements
logger.debug("Evohome Gateway not online, scanning postponed");
return;
}
Locations localEvohomeConfig = bridge.getEvohomeConfig();
for (Location location : bridge.getEvohomeConfig()) {
if (localEvohomeConfig == null) {
return;
}
for (Location location : localEvohomeConfig) {
if (location == null) {
continue;
}
for (Gateway gateway : location.getGateways()) {
for (TemperatureControlSystem tcs : gateway.getTemperatureControlSystems()) {
if (tcs == null) {
continue;
}
addDisplayDiscoveryResult(location, tcs);
for (Zone zone : tcs.getZones()) {
addZoneDiscoveryResult(location, zone);
if (zone != null) {
addZoneDiscoveryResult(location, zone);
}
}
}
}
}
stopScan();
}

View File

@ -12,6 +12,7 @@
*/
package org.openhab.binding.evohome.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingStatus;
/**
@ -20,6 +21,7 @@ import org.openhab.core.thing.ThingStatus;
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public interface AccountStatusListener {
/**

View File

@ -12,7 +12,9 @@
*/
package org.openhab.binding.evohome.internal.handler;
import org.openhab.binding.evohome.internal.api.models.v2.response.Locations;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
import org.openhab.binding.evohome.internal.configuration.EvohomeThingConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
@ -25,8 +27,9 @@ import org.openhab.core.thing.binding.BaseThingHandler;
*
* @author Jasper van Zuijlen - Initial contribution
*/
@NonNullByDefault
public abstract class BaseEvohomeHandler extends BaseThingHandler {
private EvohomeThingConfiguration configuration;
private EvohomeThingConfiguration configuration = new EvohomeThingConfiguration();
public BaseEvohomeHandler(Thing thing) {
super(thing);
@ -40,14 +43,10 @@ public abstract class BaseEvohomeHandler extends BaseThingHandler {
@Override
public void dispose() {
configuration = null;
}
public String getId() {
if (configuration != null) {
return configuration.id;
}
return null;
return configuration.id;
}
/**
@ -64,7 +63,7 @@ public abstract class BaseEvohomeHandler extends BaseThingHandler {
*
* @return The evohome brdige
*/
protected EvohomeAccountBridgeHandler getEvohomeBridge() {
protected @Nullable EvohomeAccountBridgeHandler getEvohomeBridge() {
Bridge bridge = getBridge();
if (bridge != null) {
return (EvohomeAccountBridgeHandler) bridge.getHandler();
@ -78,10 +77,10 @@ public abstract class BaseEvohomeHandler extends BaseThingHandler {
*
* @return The current evohome configuration
*/
protected Locations getEvohomeConfig() {
EvohomeAccountBridgeHandler bridge = getEvohomeBridge();
if (bridge != null) {
return bridge.getEvohomeConfig();
protected @Nullable Locations getEvohomeConfig() {
EvohomeAccountBridgeHandler bridgeAccountHandler = getEvohomeBridge();
if (bridgeAccountHandler != null) {
return bridgeAccountHandler.getEvohomeConfig();
}
return null;
@ -115,7 +114,7 @@ public abstract class BaseEvohomeHandler extends BaseThingHandler {
* @param detail The status detail value
* @param message The message to show with the status
*/
protected void updateEvohomeThingStatus(ThingStatus newStatus, ThingStatusDetail detail, String message) {
protected void updateEvohomeThingStatus(ThingStatus newStatus, ThingStatusDetail detail, @Nullable String message) {
// Prevent spamming the log file
if (!newStatus.equals(getThing().getStatus())) {
updateStatus(newStatus, detail, message);
@ -128,10 +127,7 @@ public abstract class BaseEvohomeHandler extends BaseThingHandler {
* @param configuration The configuration to check
*/
private void checkConfig() {
if (configuration == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Configuration is missing or corrupted");
} else if (configuration.id == null || configuration.id.isEmpty()) {
if (configuration.id.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Id not configured");
}
}

View File

@ -22,19 +22,21 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.evohome.internal.RunnableWithTimeout;
import org.openhab.binding.evohome.internal.api.EvohomeApiClient;
import org.openhab.binding.evohome.internal.api.models.v2.response.Gateway;
import org.openhab.binding.evohome.internal.api.models.v2.response.GatewayStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.response.LocationStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.Locations;
import org.openhab.binding.evohome.internal.api.models.v2.response.LocationsStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.TemperatureControlSystem;
import org.openhab.binding.evohome.internal.api.models.v2.response.TemperatureControlSystemStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.Zone;
import org.openhab.binding.evohome.internal.api.models.v2.response.ZoneStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Gateway;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.GatewayStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Location;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationsStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystem;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystemStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Zone;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.ZoneStatus;
import org.openhab.binding.evohome.internal.configuration.EvohomeAccountConfiguration;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
@ -54,15 +56,16 @@ import org.slf4j.LoggerFactory;
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(EvohomeAccountBridgeHandler.class);
private final HttpClient httpClient;
private EvohomeAccountConfiguration configuration;
private EvohomeApiClient apiClient;
private EvohomeAccountConfiguration configuration = new EvohomeAccountConfiguration();
private @Nullable EvohomeApiClient apiClient;
private List<AccountStatusListener> listeners = new CopyOnWriteArrayList<>();
protected ScheduledFuture<?> refreshTask;
protected @Nullable ScheduledFuture<?> refreshTask;
public EvohomeAccountBridgeHandler(Bridge thing, HttpClient httpClient) {
super(thing);
@ -73,13 +76,14 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
public void initialize() {
configuration = getConfigAs(EvohomeAccountConfiguration.class);
if (checkConfig()) {
if (checkConfig(configuration)) {
apiClient = new EvohomeApiClient(configuration, this.httpClient);
// Initialization can take a while, so kick it off on a separate thread
scheduler.schedule(() -> {
if (apiClient.login()) {
if (checkInstallationInfoHasDuplicateIds(apiClient.getInstallationInfo())) {
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null && localApiCLient.login()) {
if (checkInstallationInfoHasDuplicateIds(localApiCLient.getInstallationInfo())) {
startRefreshTask();
} else {
updateAccountStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
@ -104,24 +108,41 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
public void handleCommand(ChannelUID channelUID, Command command) {
}
public Locations getEvohomeConfig() {
return apiClient.getInstallationInfo();
public @Nullable Locations getEvohomeConfig() {
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
return localApiCLient.getInstallationInfo();
}
return null;
}
public LocationsStatus getEvohomeStatus() {
return apiClient.getInstallationStatus();
public @Nullable LocationsStatus getEvohomeStatus() {
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
return localApiCLient.getInstallationStatus();
}
return null;
}
public void setTcsMode(String tcsId, String mode) {
tryToCall(() -> apiClient.setTcsMode(tcsId, mode));
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
tryToCall(() -> localApiCLient.setTcsMode(tcsId, mode));
}
}
public void setPermanentSetPoint(String zoneId, double doubleValue) {
tryToCall(() -> apiClient.setHeatingZoneOverride(zoneId, doubleValue));
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
tryToCall(() -> localApiCLient.setHeatingZoneOverride(zoneId, doubleValue));
}
}
public void cancelSetPointOverride(String zoneId) {
tryToCall(() -> apiClient.cancelHeatingZoneOverride(zoneId));
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
tryToCall(() -> localApiCLient.cancelHeatingZoneOverride(zoneId));
}
}
public void addAccountStatusListener(AccountStatusListener listener) {
@ -164,26 +185,27 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
}
private void disposeApiClient() {
if (apiClient != null) {
apiClient.logout();
EvohomeApiClient localApiClient = apiClient;
if (localApiClient != null) {
localApiClient.logout();
this.apiClient = null;
}
apiClient = null;
}
private void disposeRefreshTask() {
if (refreshTask != null) {
refreshTask.cancel(true);
ScheduledFuture<?> localRefreshTask = refreshTask;
if (localRefreshTask != null) {
localRefreshTask.cancel(true);
this.refreshTask = null;
}
}
private boolean checkConfig() {
private boolean checkConfig(EvohomeAccountConfiguration configuration) {
String errorMessage = "";
if (configuration == null) {
errorMessage = "Configuration is missing or corrupted";
} else if (configuration.username == null || configuration.username.isEmpty()) {
if (configuration.username.isBlank()) {
errorMessage = "Username not configured";
} else if (configuration.password == null || configuration.password.isEmpty()) {
} else if (configuration.password.isBlank()) {
errorMessage = "Password not configured";
} else {
return true;
@ -202,7 +224,10 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
private void update() {
try {
apiClient.update();
EvohomeApiClient localApiCLient = apiClient;
if (localApiCLient != null) {
localApiCLient.update();
}
updateAccountStatus(ThingStatus.ONLINE);
updateThings();
} catch (Exception e) {
@ -215,7 +240,7 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
updateAccountStatus(newStatus, ThingStatusDetail.NONE, null);
}
private void updateAccountStatus(ThingStatus newStatus, ThingStatusDetail detail, String message) {
private void updateAccountStatus(ThingStatus newStatus, ThingStatusDetail detail, @Nullable String message) {
// Prevent spamming the log file
if (!newStatus.equals(getThing().getStatus())) {
updateStatus(newStatus, detail, message);
@ -236,15 +261,32 @@ public class EvohomeAccountBridgeHandler extends BaseBridgeHandler {
Map<String, String> zoneIdToTcsIdMap = new HashMap<>();
Map<String, ThingStatus> idToTcsThingsStatusMap = new HashMap<>();
// First, create a lookup table
for (LocationStatus location : apiClient.getInstallationStatus()) {
for (GatewayStatus gateway : location.getGateways()) {
for (TemperatureControlSystemStatus tcs : gateway.getTemperatureControlSystems()) {
idToTcsMap.put(tcs.getSystemId(), tcs);
tcsIdToGatewayMap.put(tcs.getSystemId(), gateway);
for (ZoneStatus zone : tcs.getZones()) {
idToZoneMap.put(zone.getZoneId(), zone);
zoneIdToTcsIdMap.put(zone.getZoneId(), tcs.getSystemId());
EvohomeApiClient localApiClient = apiClient;
if (localApiClient != null) {
// First, create a lookup table
LocationsStatus localLocationsStatus = localApiClient.getInstallationStatus();
if (localLocationsStatus != null) {
for (LocationStatus location : localLocationsStatus) {
for (GatewayStatus gateway : location.getGateways()) {
if (gateway == null) {
continue;
}
for (TemperatureControlSystemStatus tcs : gateway.getTemperatureControlSystems()) {
String systemId = tcs.getSystemId();
if (systemId != null) {
idToTcsMap.put(systemId, tcs);
tcsIdToGatewayMap.put(systemId, gateway);
}
for (ZoneStatus zone : tcs.getZones()) {
String zoneId = zone.getZoneId();
if (zoneId != null) {
idToZoneMap.put(zoneId, zone);
if (systemId != null) {
zoneIdToTcsIdMap.put(zoneId, systemId);
}
}
}
}
}
}
}

View File

@ -12,10 +12,15 @@
*/
package org.openhab.binding.evohome.internal.handler;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.evohome.internal.EvohomeBindingConstants;
import org.openhab.binding.evohome.internal.api.models.v2.response.ZoneStatus;
import org.openhab.core.library.types.DecimalType;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.ZoneStatus;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
@ -30,12 +35,14 @@ import org.openhab.core.types.RefreshType;
* @author Jasper van Zuijlen - Initial contribution
* @author Neil Renaud - Working implementation
* @author Jasper van Zuijlen - Refactor + Permanent Zone temperature setting
* @author Leo Siepel - Add UoM
*/
@NonNullByDefault
public class EvohomeHeatingZoneHandler extends BaseEvohomeHandler {
private static final int CANCEL_SET_POINT_OVERRIDE = 0;
private ThingStatus tcsStatus;
private ZoneStatus zoneStatus;
private @Nullable ThingStatus tcsStatus;
private @Nullable ZoneStatus zoneStatus;
public EvohomeHeatingZoneHandler(Thing thing) {
super(thing);
@ -46,7 +53,7 @@ public class EvohomeHeatingZoneHandler extends BaseEvohomeHandler {
super.initialize();
}
public void update(ThingStatus tcsStatus, ZoneStatus zoneStatus) {
public void update(@Nullable ThingStatus tcsStatus, @Nullable ZoneStatus zoneStatus) {
this.tcsStatus = tcsStatus;
this.zoneStatus = zoneStatus;
@ -62,11 +69,11 @@ public class EvohomeHeatingZoneHandler extends BaseEvohomeHandler {
updateEvohomeThingStatus(ThingStatus.ONLINE);
updateState(EvohomeBindingConstants.ZONE_TEMPERATURE_CHANNEL,
new DecimalType(zoneStatus.getTemperature().getTemperature()));
new QuantityType<Temperature>(zoneStatus.getTemperature().getTemperature(), SIUnits.CELSIUS));
updateState(EvohomeBindingConstants.ZONE_SET_POINT_STATUS_CHANNEL,
new StringType(zoneStatus.getHeatSetpoint().getSetpointMode()));
updateState(EvohomeBindingConstants.ZONE_SET_POINT_CHANNEL,
new DecimalType(zoneStatus.getHeatSetpoint().getTargetTemperature()));
updateState(EvohomeBindingConstants.ZONE_SET_POINT_CHANNEL, new QuantityType<Temperature>(
zoneStatus.getHeatSetpoint().getTargetTemperature(), SIUnits.CELSIUS));
}
}
@ -78,16 +85,19 @@ public class EvohomeHeatingZoneHandler extends BaseEvohomeHandler {
EvohomeAccountBridgeHandler bridge = getEvohomeBridge();
if (bridge != null) {
String channelId = channelUID.getId();
if (EvohomeBindingConstants.ZONE_SET_POINT_CHANNEL.equals(channelId)
&& command instanceof DecimalType) {
double newTemp = ((DecimalType) command).doubleValue();
if (newTemp == CANCEL_SET_POINT_OVERRIDE) {
bridge.cancelSetPointOverride(getEvohomeThingConfig().id);
} else if (newTemp < 5) {
newTemp = 5;
}
if (newTemp >= 5 && newTemp <= 35) {
bridge.setPermanentSetPoint(getEvohomeThingConfig().id, newTemp);
if (EvohomeBindingConstants.ZONE_SET_POINT_CHANNEL.equals(channelId)) {
if (command instanceof QuantityType) {
QuantityType<?> state = ((QuantityType<?>) command).toUnit(SIUnits.CELSIUS);
double newTempInCelsius = state.doubleValue();
if (newTempInCelsius == CANCEL_SET_POINT_OVERRIDE) {
bridge.cancelSetPointOverride(getEvohomeThingConfig().id);
} else if (newTempInCelsius < 5) {
newTempInCelsius = 5;
}
if (newTempInCelsius >= 5 && newTempInCelsius <= 35) {
bridge.setPermanentSetPoint(getEvohomeThingConfig().id, newTempInCelsius);
}
}
}
}

View File

@ -12,9 +12,11 @@
*/
package org.openhab.binding.evohome.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.evohome.internal.EvohomeBindingConstants;
import org.openhab.binding.evohome.internal.api.models.v2.response.GatewayStatus;
import org.openhab.binding.evohome.internal.api.models.v2.response.TemperatureControlSystemStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.GatewayStatus;
import org.openhab.binding.evohome.internal.api.models.v2.dto.response.TemperatureControlSystemStatus;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -29,9 +31,10 @@ import org.openhab.core.types.RefreshType;
* @author Jasper van Zuijlen - Initial contribution
*
*/
@NonNullByDefault
public class EvohomeTemperatureControlSystemHandler extends BaseEvohomeHandler {
private GatewayStatus gatewayStatus;
private TemperatureControlSystemStatus tcsStatus;
private @Nullable GatewayStatus gatewayStatus;
private @Nullable TemperatureControlSystemStatus tcsStatus;
public EvohomeTemperatureControlSystemHandler(Thing thing) {
super(thing);
@ -42,7 +45,7 @@ public class EvohomeTemperatureControlSystemHandler extends BaseEvohomeHandler {
super.initialize();
}
public void update(GatewayStatus gatewayStatus, TemperatureControlSystemStatus tcsStatus) {
public void update(@Nullable GatewayStatus gatewayStatus, @Nullable TemperatureControlSystemStatus tcsStatus) {
this.gatewayStatus = gatewayStatus;
this.tcsStatus = tcsStatus;

View File

@ -20,19 +20,27 @@
</state>
</channel-type>
<channel-type id="temperature">
<item-type>Number</item-type>
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current zone temperature</description>
<category>temperature</category>
<state readOnly="true" pattern="%.1f °C">
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%">
</state>
</channel-type>
<channel-type id="setpoint">
<item-type>Number</item-type>
<item-type>Number:Temperature</item-type>
<label>Set Point</label>
<description>Gets or sets the set point of this zone (0 cancels the override).</description>
<category>heating</category>
<state min="0.0" max="35.0" step="0.5" pattern="%.1f °C"/>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state min="0.0" max="35.0" step="0.5" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="setpointstatus">
<item-type>String</item-type>