]> git.basschouten.com Git - openhab-addons.git/blob
48939b1819d04b01239a906fe2f1d4303836fbe7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.evohome.internal.api;
14
15 import java.io.UnsupportedEncodingException;
16 import java.net.URLEncoder;
17 import java.util.Base64;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.concurrent.TimeoutException;
21
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;
37
38 /**
39  * Implementation of the evohome client V2 api
40  *
41  * @author Jasper van Zuijlen - Initial contribution
42  *
43  */
44 public class EvohomeApiClient {
45
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";
49
50     private final Logger logger = LoggerFactory.getLogger(EvohomeApiClient.class);
51     private final HttpClient httpClient;
52     private final EvohomeAccountConfiguration configuration;
53     private final ApiAccess apiAccess;
54
55     private Locations locations = new Locations();
56     private UserAccount useraccount;
57     private LocationsStatus locationsStatus;
58
59     /**
60      * Creates a new API client based on the V2 API interface
61      *
62      * @param configuration The configuration of the account to use
63      * @throws Exception
64      */
65     public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) throws Exception {
66         this.configuration = configuration;
67         this.httpClient = httpClient;
68
69         try {
70             httpClient.start();
71         } catch (Exception e) {
72             logger.error("Could not start http client", e);
73             throw new EvohomeApiClientException("Could not start http client", e);
74         }
75
76         apiAccess = new ApiAccess(httpClient);
77         apiAccess.setApplicationId(APPLICATION_ID);
78     }
79
80     /**
81      * Closes the current connection to the API
82      */
83     public void close() {
84         apiAccess.setAuthentication(null);
85         useraccount = null;
86         locations = null;
87         locationsStatus = null;
88
89         if (httpClient.isStarted()) {
90             try {
91                 httpClient.stop();
92             } catch (Exception e) {
93                 logger.debug("Could not stop http client.", e);
94             }
95         }
96     }
97
98     public boolean login() {
99         boolean success = authenticateWithUsername();
100
101         // If the authentication succeeded, gather the basic intel as well
102         if (success) {
103             try {
104                 useraccount = requestUserAccount();
105                 locations = requestLocations();
106             } catch (TimeoutException e) {
107                 logger.warn("Timeout while retrieving user and location information. Failing loging.");
108                 success = false;
109             }
110         } else {
111             apiAccess.setAuthentication(null);
112             logger.debug("Authorization failed");
113         }
114
115         return success;
116     }
117
118     public void logout() {
119         close();
120     }
121
122     public void update() {
123         updateAuthentication();
124         try {
125             locationsStatus = requestLocationsStatus();
126         } catch (TimeoutException e) {
127             logger.info("Timeout on update");
128         }
129     }
130
131     public Locations getInstallationInfo() {
132         return locations;
133     }
134
135     public LocationsStatus getInstallationStatus() {
136         return locationsStatus;
137     }
138
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);
143     }
144
145     public void setHeatingZoneOverride(String zoneId, double setPoint) throws TimeoutException {
146         HeatSetPoint setPointCommand = new HeatSetPointBuilder().setSetPoint(setPoint).build();
147         setHeatingZoneOverride(zoneId, setPointCommand);
148     }
149
150     public void cancelHeatingZoneOverride(String zoneId) throws TimeoutException {
151         HeatSetPoint setPointCommand = new HeatSetPointBuilder().setCancelSetPoint().build();
152         setHeatingZoneOverride(zoneId, setPointCommand);
153     }
154
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);
159     }
160
161     private UserAccount requestUserAccount() throws TimeoutException {
162         String url = EvohomeApiConstants.URL_V2_BASE + EvohomeApiConstants.URL_V2_ACCOUNT;
163         return apiAccess.doAuthenticatedGet(url, UserAccount.class);
164     }
165
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());
171
172             locations = apiAccess.doAuthenticatedGet(url, Locations.class);
173         }
174         return locations;
175     }
176
177     private LocationsStatus requestLocationsStatus() throws TimeoutException {
178         LocationsStatus locationsStatus = new LocationsStatus();
179
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);
186             }
187         }
188         return locationsStatus;
189     }
190
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";
197
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");
202
203         Authentication authentication;
204         try {
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;
210         }
211
212         apiAccess.setAuthentication(authentication);
213
214         if (authentication != null) {
215             authentication.setSystemTime(System.currentTimeMillis() / 1000);
216         }
217
218         return (authentication != null);
219     }
220
221     private boolean authenticateWithUsername() {
222         boolean result = false;
223
224         try {
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);
230         }
231
232         return result;
233     }
234
235     private boolean authenticateWithToken(String accessToken) {
236         String credentials = "refresh_token=" + accessToken;
237         return authenticate(credentials, "refresh_token");
238     }
239
240     private void updateAuthentication() {
241         Authentication authentication = apiAccess.getAuthentication();
242         if (authentication == null) {
243             authenticateWithUsername();
244         } else {
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;
249
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();
256                 }
257             }
258         }
259     }
260 }