2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.evohome.internal.api;
15 import java.io.UnsupportedEncodingException;
16 import java.net.URLEncoder;
17 import java.util.Base64;
18 import java.util.HashMap;
20 import java.util.concurrent.TimeoutException;
22 import org.eclipse.jetty.client.HttpClient;
23 import org.eclipse.jetty.http.HttpMethod;
24 import org.openhab.binding.evohome.internal.api.models.v2.request.HeatSetPoint;
25 import org.openhab.binding.evohome.internal.api.models.v2.request.HeatSetPointBuilder;
26 import org.openhab.binding.evohome.internal.api.models.v2.request.Mode;
27 import org.openhab.binding.evohome.internal.api.models.v2.request.ModeBuilder;
28 import org.openhab.binding.evohome.internal.api.models.v2.response.Authentication;
29 import org.openhab.binding.evohome.internal.api.models.v2.response.Location;
30 import org.openhab.binding.evohome.internal.api.models.v2.response.LocationStatus;
31 import org.openhab.binding.evohome.internal.api.models.v2.response.Locations;
32 import org.openhab.binding.evohome.internal.api.models.v2.response.LocationsStatus;
33 import org.openhab.binding.evohome.internal.api.models.v2.response.UserAccount;
34 import org.openhab.binding.evohome.internal.configuration.EvohomeAccountConfiguration;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
39 * Implementation of the evohome client V2 api
41 * @author Jasper van Zuijlen - Initial contribution
44 public class EvohomeApiClient {
46 private static final String APPLICATION_ID = "b013aa26-9724-4dbd-8897-048b9aada249";
47 private static final String CLIENT_ID = "4a231089-d2b6-41bd-a5eb-16a0a422b999";
48 private static final String CLIENT_SECRET = "1a15cdb8-42de-407b-add0-059f92c530cb";
50 private final Logger logger = LoggerFactory.getLogger(EvohomeApiClient.class);
51 private final HttpClient httpClient;
52 private final EvohomeAccountConfiguration configuration;
53 private final ApiAccess apiAccess;
55 private Locations locations = new Locations();
56 private UserAccount useraccount;
57 private LocationsStatus locationsStatus;
60 * Creates a new API client based on the V2 API interface
62 * @param configuration The configuration of the account to use
65 public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) throws Exception {
66 this.configuration = configuration;
67 this.httpClient = httpClient;
71 } catch (Exception e) {
72 logger.error("Could not start http client", e);
73 throw new EvohomeApiClientException("Could not start http client", e);
76 apiAccess = new ApiAccess(httpClient);
77 apiAccess.setApplicationId(APPLICATION_ID);
81 * Closes the current connection to the API
84 apiAccess.setAuthentication(null);
87 locationsStatus = null;
89 if (httpClient.isStarted()) {
92 } catch (Exception e) {
93 logger.debug("Could not stop http client.", e);
98 public boolean login() {
99 boolean success = authenticateWithUsername();
101 // If the authentication succeeded, gather the basic intel as well
104 useraccount = requestUserAccount();
105 locations = requestLocations();
106 } catch (TimeoutException e) {
107 logger.warn("Timeout while retrieving user and location information. Failing loging.");
111 apiAccess.setAuthentication(null);
112 logger.debug("Authorization failed");
118 public void logout() {
122 public void update() {
123 updateAuthentication();
125 locationsStatus = requestLocationsStatus();
126 } catch (TimeoutException e) {
127 logger.info("Timeout on update");
131 public Locations getInstallationInfo() {
135 public LocationsStatus getInstallationStatus() {
136 return locationsStatus;
139 public void setTcsMode(String tcsId, String mode) throws TimeoutException {
140 String url = String.format(EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_MODE, tcsId);
141 Mode modeCommand = new ModeBuilder().setMode(mode).build();
142 apiAccess.doAuthenticatedPut(url, modeCommand);
145 public void setHeatingZoneOverride(String zoneId, double setPoint) throws TimeoutException {
146 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setSetPoint(setPoint).build();
147 setHeatingZoneOverride(zoneId, setPointCommand);
150 public void cancelHeatingZoneOverride(String zoneId) throws TimeoutException {
151 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setCancelSetPoint().build();
152 setHeatingZoneOverride(zoneId, setPointCommand);
155 private void setHeatingZoneOverride(String zoneId, HeatSetPoint heatSetPoint) throws TimeoutException {
156 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_HEAT_SETPOINT;
157 url = String.format(url, zoneId);
158 apiAccess.doAuthenticatedPut(url, heatSetPoint);
161 private UserAccount requestUserAccount() throws TimeoutException {
162 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_ACCOUNT;
163 return apiAccess.doAuthenticatedGet(url, UserAccount.class);
166 private Locations requestLocations() throws TimeoutException {
167 Locations locations = new Locations();
168 if (useraccount != null) {
169 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_INSTALLATION_INFO;
170 url = String.format(url, useraccount.getUserId());
172 locations = apiAccess.doAuthenticatedGet(url, Locations.class);
177 private LocationsStatus requestLocationsStatus() throws TimeoutException {
178 LocationsStatus locationsStatus = new LocationsStatus();
180 if (locations != null) {
181 for (Location location : locations) {
182 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_LOCATION_STATUS;
183 url = String.format(url, location.getLocationInfo().getLocationId());
184 LocationStatus status = apiAccess.doAuthenticatedGet(url, LocationStatus.class);
185 locationsStatus.add(status);
188 return locationsStatus;
191 private boolean authenticate(String credentials, String grantType) {
192 String data = credentials + "&" + "Host=rs.alarmnet.com%2F&" + "Pragma=no-cache&"
193 + "Cache-Control=no-store+no-cache&"
194 + "scope=EMEA-V1-Basic+EMEA-V1-Anonymous+EMEA-V1-Get-Current-User-Account&" + "grant_type=" + grantType
195 + "&" + "Content-Type=application%2Fx-www-form-urlencoded%3B+charset%3Dutf-8&"
196 + "Connection=Keep-Alive";
198 Map<String, String> headers = new HashMap<>();
199 String basicAuth = Base64.getEncoder().encodeToString((CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
200 headers.put("Authorization", "Basic " + basicAuth);
201 headers.put("Accept", "application/json, application/xml, text/json, text/x-json, text/javascript, text/xml");
203 Authentication authentication;
205 authentication = apiAccess.doRequest(HttpMethod.POST, EvohomeApiConstants.URL_V2_AUTH, headers, data,
206 "application/x-www-form-urlencoded", Authentication.class);
207 } catch (TimeoutException e) {
208 // A timeout is not a successful login as well
209 authentication = null;
212 apiAccess.setAuthentication(authentication);
214 if (authentication != null) {
215 authentication.setSystemTime(System.currentTimeMillis() / 1000);
218 return (authentication != null);
221 private boolean authenticateWithUsername() {
222 boolean result = false;
225 String credentials = "Username=" + URLEncoder.encode(configuration.username, "UTF-8") + "&" + "Password="
226 + URLEncoder.encode(configuration.password, "UTF-8");
227 result = authenticate(credentials, "password");
228 } catch (UnsupportedEncodingException e) {
229 logger.error("Credential conversion failed", e);
235 private boolean authenticateWithToken(String accessToken) {
236 String credentials = "refresh_token=" + accessToken;
237 return authenticate(credentials, "refresh_token");
240 private void updateAuthentication() {
241 Authentication authentication = apiAccess.getAuthentication();
242 if (authentication == null) {
243 authenticateWithUsername();
245 // Compare current time to the expiration time minus four intervals for slack
246 long currentTime = System.currentTimeMillis() / 1000;
247 long expiration = authentication.getSystemTime() + authentication.getExpiresIn();
248 expiration -= 4 * configuration.refreshInterval;
250 // Update the access token just before it expires, but fall back to username and password
251 // when it fails (i.e. refresh token had been invalidated)
252 if (currentTime > expiration) {
253 authenticateWithToken(authentication.getRefreshToken());
254 if (apiAccess.getAuthentication() == null) {
255 authenticateWithUsername();