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.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.eclipse.jetty.http.HttpMethod;
26 import org.openhab.binding.evohome.internal.api.models.v2.dto.request.HeatSetPoint;
27 import org.openhab.binding.evohome.internal.api.models.v2.dto.request.HeatSetPointBuilder;
28 import org.openhab.binding.evohome.internal.api.models.v2.dto.request.Mode;
29 import org.openhab.binding.evohome.internal.api.models.v2.dto.request.ModeBuilder;
30 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Authentication;
31 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Location;
32 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationStatus;
33 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.Locations;
34 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.LocationsStatus;
35 import org.openhab.binding.evohome.internal.api.models.v2.dto.response.UserAccount;
36 import org.openhab.binding.evohome.internal.configuration.EvohomeAccountConfiguration;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * Implementation of the evohome client V2 api
43 * @author Jasper van Zuijlen - Initial contribution
47 public class EvohomeApiClient {
49 private static final String APPLICATION_ID = "b013aa26-9724-4dbd-8897-048b9aada249";
50 private static final String CLIENT_ID = "4a231089-d2b6-41bd-a5eb-16a0a422b999";
51 private static final String CLIENT_SECRET = "1a15cdb8-42de-407b-add0-059f92c530cb";
53 private final Logger logger = LoggerFactory.getLogger(EvohomeApiClient.class);
54 private final EvohomeAccountConfiguration configuration;
55 private final ApiAccess apiAccess;
57 private Locations locations = new Locations();
58 private @Nullable UserAccount useraccount;
59 private @Nullable LocationsStatus locationsStatus;
62 * Creates a new API client based on the V2 API interface
64 * @param configuration The configuration of the account to use
66 public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) {
67 this.configuration = configuration;
68 apiAccess = new ApiAccess(httpClient);
69 apiAccess.setApplicationId(APPLICATION_ID);
73 * Closes the current connection to the API
76 apiAccess.setAuthentication(null);
78 locations = new Locations();
79 locationsStatus = null;
82 public boolean login() {
83 boolean success = authenticateWithUsername();
85 // If the authentication succeeded, gather the basic intel as well
88 useraccount = requestUserAccount();
89 locations = requestLocations();
90 } catch (TimeoutException e) {
91 logger.warn("Timeout while retrieving user and location information. Failing loging.");
95 apiAccess.setAuthentication(null);
96 logger.debug("Authorization failed");
102 public void logout() {
106 public void update() {
107 updateAuthentication();
109 locationsStatus = requestLocationsStatus();
110 } catch (TimeoutException e) {
111 logger.info("Timeout on update");
115 public Locations getInstallationInfo() {
119 public @Nullable LocationsStatus getInstallationStatus() {
120 return locationsStatus;
123 public void setTcsMode(String tcsId, String mode) throws TimeoutException {
124 String url = String.format(EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_MODE, tcsId);
125 Mode modeCommand = new ModeBuilder().setMode(mode).build();
126 apiAccess.doAuthenticatedPut(url, modeCommand);
129 public void setHeatingZoneOverride(String zoneId, double setPoint) throws TimeoutException {
130 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setSetPoint(setPoint).build();
131 setHeatingZoneOverride(zoneId, setPointCommand);
134 public void cancelHeatingZoneOverride(String zoneId) throws TimeoutException {
135 HeatSetPoint setPointCommand = new HeatSetPointBuilder().setCancelSetPoint().build();
136 setHeatingZoneOverride(zoneId, setPointCommand);
139 private void setHeatingZoneOverride(String zoneId, HeatSetPoint heatSetPoint) throws TimeoutException {
140 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_HEAT_SETPOINT;
141 url = String.format(url, zoneId);
142 apiAccess.doAuthenticatedPut(url, heatSetPoint);
145 private @Nullable UserAccount requestUserAccount() throws TimeoutException {
146 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_ACCOUNT;
147 return apiAccess.doAuthenticatedGet(url, UserAccount.class);
150 private Locations requestLocations() throws TimeoutException {
151 Locations locations = null;
152 UserAccount localAccount = useraccount;
153 if (localAccount != null) {
154 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_INSTALLATION_INFO;
155 url = String.format(url, localAccount.getUserId());
157 locations = apiAccess.doAuthenticatedGet(url, Locations.class);
159 return locations != null ? locations : new Locations();
162 private LocationsStatus requestLocationsStatus() throws TimeoutException {
163 LocationsStatus locationsStatus = new LocationsStatus();
165 for (Location location : locations) {
166 String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_LOCATION_STATUS;
167 url = String.format(url, location.getLocationInfo().getLocationId());
168 LocationStatus status = apiAccess.doAuthenticatedGet(url, LocationStatus.class);
169 locationsStatus.add(status);
172 return locationsStatus;
175 private boolean authenticate(String credentials, String grantType) {
176 String data = credentials + "&" + "Host=rs.alarmnet.com%2F&" + "Pragma=no-cache&"
177 + "Cache-Control=no-store+no-cache&"
178 + "scope=EMEA-V1-Basic+EMEA-V1-Anonymous+EMEA-V1-Get-Current-User-Account&" + "grant_type=" + grantType
179 + "&" + "Content-Type=application%2Fx-www-form-urlencoded%3B+charset%3Dutf-8&"
180 + "Connection=Keep-Alive";
182 Map<String, String> headers = new HashMap<>();
183 String basicAuth = Base64.getEncoder().encodeToString((CLIENT_ID + ":" + CLIENT_SECRET).getBytes());
184 headers.put("Authorization", "Basic " + basicAuth);
185 headers.put("Accept", "application/json, application/xml, text/json, text/x-json, text/javascript, text/xml");
187 Authentication authentication;
189 authentication = apiAccess.doRequest(HttpMethod.POST, EvohomeApiConstants.URL_V2_AUTH, headers, data,
190 "application/x-www-form-urlencoded", Authentication.class);
191 } catch (TimeoutException e) {
192 // A timeout is not a successful login as well
193 authentication = null;
196 apiAccess.setAuthentication(authentication);
198 if (authentication != null) {
199 authentication.setSystemTime(System.currentTimeMillis() / 1000);
202 return (authentication != null);
205 private boolean authenticateWithUsername() {
206 String credentials = "Username=" + URLEncoder.encode(configuration.username, StandardCharsets.UTF_8) + "&"
207 + "Password=" + URLEncoder.encode(configuration.password, StandardCharsets.UTF_8);
208 return authenticate(credentials, "password");
211 private boolean authenticateWithToken(String accessToken) {
212 String credentials = "refresh_token=" + accessToken;
213 return authenticate(credentials, "refresh_token");
216 private void updateAuthentication() {
217 Authentication authentication = apiAccess.getAuthentication();
218 if (authentication == null) {
219 authenticateWithUsername();
221 // Compare current time to the expiration time minus four intervals for slack
222 long currentTime = System.currentTimeMillis() / 1000;
223 long expiration = authentication.getSystemTime() + authentication.getExpiresIn();
224 expiration -= 4 * configuration.refreshInterval;
226 // Update the access token just before it expires, but fall back to username and password
227 // when it fails (i.e. refresh token had been invalidated)
228 if (currentTime > expiration) {
229 authenticateWithToken(authentication.getRefreshToken());
230 if (apiAccess.getAuthentication() == null) {
231 authenticateWithUsername();