]> git.basschouten.com Git - openhab-addons.git/blob
eaceccf87a1f661beda76be26b48fa19e54178db
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.net.URLEncoder;
16 import java.nio.charset.StandardCharsets;
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.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;
39
40 /**
41  * Implementation of the evohome client V2 api
42  *
43  * @author Jasper van Zuijlen - Initial contribution
44  *
45  */
46 @NonNullByDefault
47 public class EvohomeApiClient {
48
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";
52
53     private final Logger logger = LoggerFactory.getLogger(EvohomeApiClient.class);
54     private final EvohomeAccountConfiguration configuration;
55     private final ApiAccess apiAccess;
56
57     private Locations locations = new Locations();
58     private @Nullable UserAccount useraccount;
59     private @Nullable LocationsStatus locationsStatus;
60
61     /**
62      * Creates a new API client based on the V2 API interface
63      *
64      * @param configuration The configuration of the account to use
65      */
66     public EvohomeApiClient(EvohomeAccountConfiguration configuration, HttpClient httpClient) {
67         this.configuration = configuration;
68         apiAccess = new ApiAccess(httpClient);
69         apiAccess.setApplicationId(APPLICATION_ID);
70     }
71
72     /**
73      * Closes the current connection to the API
74      */
75     public void close() {
76         apiAccess.setAuthentication(null);
77         useraccount = null;
78         locations = new Locations();
79         locationsStatus = null;
80     }
81
82     public boolean login() {
83         boolean success = authenticateWithUsername();
84
85         // If the authentication succeeded, gather the basic intel as well
86         if (success) {
87             try {
88                 useraccount = requestUserAccount();
89                 locations = requestLocations();
90             } catch (TimeoutException e) {
91                 logger.warn("Timeout while retrieving user and location information. Failing loging.");
92                 success = false;
93             }
94         } else {
95             apiAccess.setAuthentication(null);
96             logger.debug("Authorization failed");
97         }
98
99         return success;
100     }
101
102     public void logout() {
103         close();
104     }
105
106     public void update() {
107         updateAuthentication();
108         try {
109             locationsStatus = requestLocationsStatus();
110         } catch (TimeoutException e) {
111             logger.info("Timeout on update");
112         }
113     }
114
115     public Locations getInstallationInfo() {
116         return locations;
117     }
118
119     public @Nullable LocationsStatus getInstallationStatus() {
120         return locationsStatus;
121     }
122
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);
127     }
128
129     public void setHeatingZoneOverride(String zoneId, double setPoint) throws TimeoutException {
130         HeatSetPoint setPointCommand = new HeatSetPointBuilder().setSetPoint(setPoint).build();
131         setHeatingZoneOverride(zoneId, setPointCommand);
132     }
133
134     public void cancelHeatingZoneOverride(String zoneId) throws TimeoutException {
135         HeatSetPoint setPointCommand = new HeatSetPointBuilder().setCancelSetPoint().build();
136         setHeatingZoneOverride(zoneId, setPointCommand);
137     }
138
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);
143     }
144
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);
148     }
149
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());
156
157             locations = apiAccess.doAuthenticatedGet(url, Locations.class);
158         }
159         return locations != null ? locations : new Locations();
160     }
161
162     private LocationsStatus requestLocationsStatus() throws TimeoutException {
163         LocationsStatus locationsStatus = new LocationsStatus();
164
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);
170         }
171
172         return locationsStatus;
173     }
174
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";
181
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");
186
187         Authentication authentication;
188         try {
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;
194         }
195
196         apiAccess.setAuthentication(authentication);
197
198         if (authentication != null) {
199             authentication.setSystemTime(System.currentTimeMillis() / 1000);
200         }
201
202         return (authentication != null);
203     }
204
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");
209     }
210
211     private boolean authenticateWithToken(String accessToken) {
212         String credentials = "refresh_token=" + accessToken;
213         return authenticate(credentials, "refresh_token");
214     }
215
216     private void updateAuthentication() {
217         Authentication authentication = apiAccess.getAuthentication();
218         if (authentication == null) {
219             authenticateWithUsername();
220         } else {
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;
225
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();
232                 }
233             }
234         }
235     }
236 }