2 * Copyright (c) 2010-2023 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.net.URLEncoder;
16 import java.nio.charset.StandardCharsets;
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 EvohomeAccountConfiguration configuration;
52 private final ApiAccess apiAccess;
54 private Locations locations = new Locations();
55 private UserAccount useraccount;
56 private LocationsStatus locationsStatus;
59 * Creates a new API client based on the V2 API interface
61 * @param configuration The configuration of the account to use
63 public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) {
64 this.configuration = configuration;
65 apiAccess = new ApiAccess(httpClient);
66 apiAccess.setApplicationId(APPLICATION_ID);
70 * Closes the current connection to the API
73 apiAccess.setAuthentication(null);
76 locationsStatus = null;
79 public boolean login() {
80 boolean success = authenticateWithUsername();
82 // If the authentication succeeded, gather the basic intel as well
85 useraccount = requestUserAccount();
86 locations = requestLocations();
87 } catch (TimeoutException e) {
88 logger.warn("Timeout while retrieving user and location information. Failing loging.");
92 apiAccess.setAuthentication(null);
93 logger.debug("Authorization failed");
99 public void logout() {
103 public void update() {
104 updateAuthentication();
106 locationsStatus = requestLocationsStatus();
107 } catch (TimeoutException e) {
108 logger.info("Timeout on update");
112 public Locations getInstallationInfo() {
116 public LocationsStatus getInstallationStatus() {
117 return locationsStatus;
120 public void setTcsMode(String tcsId, String mode) throws TimeoutException {
121 String url = String.format(EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_MODE, tcsId);
122 Mode modeCommand = new ModeBuilder().setMode(mode).build();
123 apiAccess.doAuthenticatedPut(url, modeCommand);
126 public void setHeatingZoneOverride(String zoneId, double setPoint) throws TimeoutException {
127 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setSetPoint(setPoint).build();
128 setHeatingZoneOverride(zoneId, setPointCommand);
131 public void cancelHeatingZoneOverride(String zoneId) throws TimeoutException {
132 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setCancelSetPoint().build();
133 setHeatingZoneOverride(zoneId, setPointCommand);
136 private void setHeatingZoneOverride(String zoneId, HeatSetPoint heatSetPoint) throws TimeoutException {
137 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_HEAT_SETPOINT;
138 url = String.format(url, zoneId);
139 apiAccess.doAuthenticatedPut(url, heatSetPoint);
142 private UserAccount requestUserAccount() throws TimeoutException {
143 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_ACCOUNT;
144 return apiAccess.doAuthenticatedGet(url, UserAccount.class);
147 private Locations requestLocations() throws TimeoutException {
148 Locations locations = new Locations();
149 if (useraccount != null) {
150 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_INSTALLATION_INFO;
151 url = String.format(url, useraccount.getUserId());
153 locations = apiAccess.doAuthenticatedGet(url, Locations.class);
158 private LocationsStatus requestLocationsStatus() throws TimeoutException {
159 LocationsStatus locationsStatus = new LocationsStatus();
161 if (locations != null) {
162 for (Location location : locations) {
163 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_LOCATION_STATUS;
164 url = String.format(url, location.getLocationInfo().getLocationId());
165 LocationStatus status = apiAccess.doAuthenticatedGet(url, LocationStatus.class);
166 locationsStatus.add(status);
169 return locationsStatus;
172 private boolean authenticate(String credentials, String grantType) {
173 String data = credentials + "&" + "Host=rs.alarmnet.com%2F&" + "Pragma=no-cache&"
174 + "Cache-Control=no-store+no-cache&"
175 + "scope=EMEA-V1-Basic+EMEA-V1-Anonymous+EMEA-V1-Get-Current-User-Account&" + "grant_type=" + grantType
176 + "&" + "Content-Type=application%2Fx-www-form-urlencoded%3B+charset%3Dutf-8&"
177 + "Connection=Keep-Alive";
179 Map<String, String> headers = new HashMap<>();
180 String basicAuth = Base64.getEncoder().encodeToString((CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
181 headers.put("Authorization", "Basic " + basicAuth);
182 headers.put("Accept", "application/json, application/xml, text/json, text/x-json, text/javascript, text/xml");
184 Authentication authentication;
186 authentication = apiAccess.doRequest(HttpMethod.POST, EvohomeApiConstants.URL_V2_AUTH, headers, data,
187 "application/x-www-form-urlencoded", Authentication.class);
188 } catch (TimeoutException e) {
189 // A timeout is not a successful login as well
190 authentication = null;
193 apiAccess.setAuthentication(authentication);
195 if (authentication != null) {
196 authentication.setSystemTime(System.currentTimeMillis() / 1000);
199 return (authentication != null);
202 private boolean authenticateWithUsername() {
203 String credentials = "Username=" + URLEncoder.encode(configuration.username, StandardCharsets.UTF_8) + "&"
204 + "Password=" + URLEncoder.encode(configuration.password, StandardCharsets.UTF_8);
205 return authenticate(credentials, "password");
208 private boolean authenticateWithToken(String accessToken) {
209 String credentials = "refresh_token=" + accessToken;
210 return authenticate(credentials, "refresh_token");
213 private void updateAuthentication() {
214 Authentication authentication = apiAccess.getAuthentication();
215 if (authentication == null) {
216 authenticateWithUsername();
218 // Compare current time to the expiration time minus four intervals for slack
219 long currentTime = System.currentTimeMillis() / 1000;
220 long expiration = authentication.getSystemTime() + authentication.getExpiresIn();
221 expiration -= 4 * configuration.refreshInterval;
223 // Update the access token just before it expires, but fall back to username and password
224 // when it fails (i.e. refresh token had been invalidated)
225 if (currentTime > expiration) {
226 authenticateWithToken(authentication.getRefreshToken());
227 if (apiAccess.getAuthentication() == null) {
228 authenticateWithUsername();