]> git.basschouten.com Git - openhab-addons.git/blob
587ee3085435627c5e4bc318a40da763f7e1b80d
[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.sleepiq.internal.api.impl;
14
15 import java.net.CookieStore;
16 import java.time.ZonedDateTime;
17 import java.time.format.DateTimeFormatter;
18 import java.util.HashMap;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.util.StringContentProvider;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.sleepiq.internal.api.CommunicationException;
35 import org.openhab.binding.sleepiq.internal.api.Configuration;
36 import org.openhab.binding.sleepiq.internal.api.LoginException;
37 import org.openhab.binding.sleepiq.internal.api.ResponseFormatException;
38 import org.openhab.binding.sleepiq.internal.api.SleepIQ;
39 import org.openhab.binding.sleepiq.internal.api.UnauthorizedException;
40 import org.openhab.binding.sleepiq.internal.api.dto.Bed;
41 import org.openhab.binding.sleepiq.internal.api.dto.BedsResponse;
42 import org.openhab.binding.sleepiq.internal.api.dto.Failure;
43 import org.openhab.binding.sleepiq.internal.api.dto.FamilyStatusResponse;
44 import org.openhab.binding.sleepiq.internal.api.dto.LoginInfo;
45 import org.openhab.binding.sleepiq.internal.api.dto.LoginRequest;
46 import org.openhab.binding.sleepiq.internal.api.dto.PauseModeResponse;
47 import org.openhab.binding.sleepiq.internal.api.dto.SleepDataResponse;
48 import org.openhab.binding.sleepiq.internal.api.dto.SleepNumberRequest;
49 import org.openhab.binding.sleepiq.internal.api.dto.Sleeper;
50 import org.openhab.binding.sleepiq.internal.api.dto.SleepersResponse;
51 import org.openhab.binding.sleepiq.internal.api.enums.Side;
52 import org.openhab.binding.sleepiq.internal.api.enums.SleepDataInterval;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 import com.google.gson.Gson;
57 import com.google.gson.JsonSyntaxException;
58
59 /**
60  * The {@link SleepIQImpl} class handles all interactions with the sleepiq service.
61  *
62  * @author Gregory Moyer - Initial contribution
63  */
64 @NonNullByDefault
65 public class SleepIQImpl implements SleepIQ {
66     private static final String PARAM_KEY = "_k";
67     private static final String USER_AGENT = "SleepIQ/1593766370 CFNetwork/1185.2 Darwin/20.0.0";
68
69     private static final Gson GSON = GsonGenerator.create(false);
70
71     private final Logger logger = LoggerFactory.getLogger(SleepIQImpl.class);
72
73     private final HttpClient httpClient;
74     private final CookieStore cookieStore;
75
76     protected final Configuration config;
77
78     private final LoginRequest loginRequest = new LoginRequest();
79     private volatile @Nullable LoginInfo loginInfo;
80
81     public SleepIQImpl(Configuration config, HttpClient httpClient) {
82         this.config = config;
83         this.httpClient = httpClient;
84         cookieStore = httpClient.getCookieStore();
85         loginRequest.setLogin(config.getUsername());
86         loginRequest.setPassword(config.getPassword());
87     }
88
89     @Override
90     public void shutdown() {
91         cookieStore.removeAll();
92     }
93
94     @Override
95     public @Nullable LoginInfo login() throws LoginException, UnauthorizedException {
96         logger.trace("SleepIQ: login: loginInfo={}", loginInfo);
97         if (loginInfo == null) {
98             synchronized (this) {
99                 if (loginInfo == null) {
100                     Request request = httpClient.newRequest(config.getBaseUri()).path(Endpoints.login())
101                             .agent(USER_AGENT).header(HttpHeader.CONTENT_TYPE, "application/json")
102                             .timeout(10, TimeUnit.SECONDS).method(HttpMethod.PUT)
103                             .content(new StringContentProvider(GSON.toJson(loginRequest)), "application/json");
104                     logger.trace("SleepIQ: login: request url={}", request.getURI());
105
106                     try {
107                         ContentResponse response = request.send();
108                         logger.debug("SleepIQ: login: status={}, content={}", response.getStatus(),
109                                 response.getContentAsString());
110                         if (isUnauthorized(response)) {
111                             Failure failure = GSON.fromJson(response.getContentAsString(), Failure.class);
112                             String message = failure != null ? failure.getError().getMessage() : "Login unauthorized";
113                             throw new UnauthorizedException(message);
114                         }
115                         if (isNotOk(response)) {
116                             Failure failure = GSON.fromJson(response.getContentAsString(), Failure.class);
117                             String message = failure != null ? failure.getError().getMessage() : "Login failed";
118                             throw new LoginException(message);
119                         }
120                         try {
121                             loginInfo = GSON.fromJson(response.getContentAsString(), LoginInfo.class);
122                         } catch (JsonSyntaxException e) {
123                             throw new LoginException("Failed to parse 'login' response");
124                         }
125                     } catch (InterruptedException | TimeoutException | ExecutionException e) {
126                         logger.info("SleepIQ: login: Login failed message={}", e.getMessage(), e);
127                         throw new LoginException("Problem communicating with SleepIQ cloud service");
128                     }
129                 }
130             }
131         }
132         return loginInfo;
133     }
134
135     @Override
136     public List<Bed> getBeds() throws LoginException, CommunicationException, ResponseFormatException {
137         try {
138             String contentResponse = cloudRequest(Endpoints.bed());
139             BedsResponse response = GSON.fromJson(contentResponse, BedsResponse.class);
140             if (response != null) {
141                 return response.getBeds();
142             } else {
143                 throw new ResponseFormatException("Failed to get a valid 'beds' response from cloud");
144             }
145         } catch (JsonSyntaxException e) {
146             throw new ResponseFormatException("Failed to parse 'beds' response");
147         }
148     }
149
150     @Override
151     public FamilyStatusResponse getFamilyStatus()
152             throws LoginException, ResponseFormatException, CommunicationException {
153         try {
154             String contentResponse = cloudRequest(Endpoints.familyStatus());
155             FamilyStatusResponse response = GSON.fromJson(contentResponse, FamilyStatusResponse.class);
156             if (response != null) {
157                 return response;
158             } else {
159                 throw new ResponseFormatException("Failed to get a valid 'familyStatus' response from cloud");
160             }
161         } catch (JsonSyntaxException e) {
162             throw new ResponseFormatException("Failed to parse 'familyStatus' response");
163         }
164     }
165
166     @Override
167     public List<Sleeper> getSleepers() throws LoginException, ResponseFormatException, CommunicationException {
168         try {
169             String contentResponse = cloudRequest(Endpoints.sleeper());
170             SleepersResponse response = GSON.fromJson(contentResponse, SleepersResponse.class);
171             if (response != null) {
172                 return response.getSleepers();
173             } else {
174                 throw new ResponseFormatException("Failed to get a valid 'sleepers' response from cloud");
175             }
176         } catch (JsonSyntaxException e) {
177             throw new ResponseFormatException("Failed to parse 'sleepers' response");
178         }
179     }
180
181     @Override
182     public PauseModeResponse getPauseMode(String bedId)
183             throws LoginException, ResponseFormatException, CommunicationException {
184         try {
185             String contentResponse = cloudRequest(Endpoints.pauseMode(bedId));
186             PauseModeResponse response = GSON.fromJson(contentResponse, PauseModeResponse.class);
187             if (response != null) {
188                 return response;
189             } else {
190                 throw new ResponseFormatException("Failed to get a valid 'pauseMode' response from cloud");
191             }
192         } catch (JsonSyntaxException e) {
193             throw new ResponseFormatException("Failed to parse 'pauseMode' response");
194         }
195     }
196
197     @Override
198     public SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval)
199             throws LoginException, ResponseFormatException, CommunicationException {
200         try {
201             Map<String, String> parameters = new HashMap<>();
202             parameters.put("interval", interval.value());
203             parameters.put("sleeper", sleeperId);
204             parameters.put("includeSlices", "false");
205             parameters.put("date", formatSleepDataDate(ZonedDateTime.now()));
206             String contentResponse = cloudRequest(Endpoints.sleepData(), parameters);
207             SleepDataResponse response = GSON.fromJson(contentResponse, SleepDataResponse.class);
208             if (response != null) {
209                 return response;
210             } else {
211                 throw new ResponseFormatException("Failed to get a valid 'sleepData' response from cloud");
212             }
213         } catch (JsonSyntaxException e) {
214             throw new ResponseFormatException("Failed to parse 'sleepData' response");
215         }
216     }
217
218     private String formatSleepDataDate(ZonedDateTime zonedDateTime) {
219         return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(zonedDateTime);
220     }
221
222     @Override
223     public void setSleepNumber(String bedId, Side side, int sleepNumber)
224             throws LoginException, ResponseFormatException, CommunicationException {
225         String body = GSON.toJson(new SleepNumberRequest().withBedId(bedId).withSleepNumber(sleepNumber).withSide(side),
226                 SleepNumberRequest.class);
227         logger.debug("SleepIQ: setSleepNumber: Request body={}", body);
228         cloudRequest(Endpoints.setSleepNumber(bedId), null, body);
229     }
230
231     @Override
232     public void setPauseMode(String bedId, boolean pauseMode)
233             throws LoginException, ResponseFormatException, CommunicationException {
234         logger.debug("SleepIQ: setPauseMode: command={}", pauseMode);
235         Map<String, String> requestParameters = new HashMap<>();
236         requestParameters.put("mode", pauseMode ? "on" : "off");
237         cloudRequest(Endpoints.setPauseMode(bedId), requestParameters, "");
238     }
239
240     private String cloudRequest(String endpoint)
241             throws LoginException, ResponseFormatException, CommunicationException {
242         return cloudRequest(endpoint, null, null);
243     }
244
245     private String cloudRequest(String endpoint, Map<String, String> parameters)
246             throws LoginException, ResponseFormatException, CommunicationException {
247         return cloudRequest(endpoint, parameters, null);
248     }
249
250     private String cloudRequest(String endpoint, @Nullable Map<String, String> parameters, @Nullable String body)
251             throws LoginException, ResponseFormatException, CommunicationException {
252         logger.debug("SleepIQ: cloudGetRequest: Invoke endpoint={}", endpoint);
253         ContentResponse response = (body == null ? doGet(endpoint, parameters) : doPut(endpoint, parameters, body));
254         if (isUnauthorized(response)) {
255             logger.debug("SleepIQ: cloudGetRequest: UNAUTHORIZED, reset login");
256             // Force new login and try again
257             resetLogin();
258             response = (body == null ? doGet(endpoint, parameters) : doPut(endpoint, parameters, body));
259         }
260         if (isNotOk(response)) {
261             throw new ResponseFormatException(String.format("Cloud API returned error: status=%d, message=%s",
262                     response.getStatus(), HttpStatus.getCode(response.getStatus()).getMessage()));
263         }
264         return response.getContentAsString();
265     }
266
267     private ContentResponse doGet(String endpoint, @Nullable Map<String, String> parameters)
268             throws CommunicationException, LoginException {
269         LoginInfo login = login();
270         Request request = httpClient.newRequest(config.getBaseUri()).path(endpoint).param(PARAM_KEY, login.getKey())
271                 .agent(USER_AGENT).header(HttpHeader.CONTENT_TYPE, "application/json").timeout(10, TimeUnit.SECONDS)
272                 .method(HttpMethod.GET);
273         return doRequest(request, parameters);
274     }
275
276     private ContentResponse doPut(String endpoint, @Nullable Map<String, String> parameters, String body)
277             throws CommunicationException, LoginException {
278         LoginInfo login = login();
279         Request request = httpClient.newRequest(config.getBaseUri()).path(endpoint).param(PARAM_KEY, login.getKey())
280                 .agent(USER_AGENT).header(HttpHeader.CONTENT_TYPE, "application/json").timeout(10, TimeUnit.SECONDS)
281                 .method(HttpMethod.PUT).content(new StringContentProvider(body), "application/json");
282         return doRequest(request, parameters);
283     }
284
285     private ContentResponse doRequest(Request request, @Nullable Map<String, String> parameters)
286             throws CommunicationException {
287         try {
288             if (parameters != null) {
289                 for (String key : parameters.keySet()) {
290                     request.param(key, parameters.get(key));
291                 }
292             }
293             addCookiesToRequest(request);
294             logger.debug("SleepIQ: doPut: request url={}", request.getURI());
295             ContentResponse response = request.send();
296             logger.trace("SleepIQ: doPut: status={} response={}", response.getStatus(), response.getContentAsString());
297             return response;
298         } catch (InterruptedException | TimeoutException | ExecutionException e) {
299             logger.debug("SleepIQ: doPut: Exception message={}", e.getMessage(), e);
300             throw new CommunicationException("Communication error while accessing API: " + e.getMessage());
301         }
302     }
303
304     private void addCookiesToRequest(Request request) {
305         cookieStore.get(config.getBaseUri()).forEach(cookie -> {
306             request.cookie(cookie);
307         });
308     }
309
310     private boolean isUnauthorized(ContentResponse response) {
311         return response.getStatus() == HttpStatus.Code.UNAUTHORIZED.getCode();
312     }
313
314     private boolean isNotOk(ContentResponse response) {
315         return response.getStatus() != HttpStatus.Code.OK.getCode();
316     }
317
318     private synchronized void resetLogin() {
319         logger.debug("SleepIQ: resetLogin: Set loginInfo=null to force login on next transaction");
320         loginInfo = null;
321     }
322 }