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.sleepiq.internal.api.impl;
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;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
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;
56 import com.google.gson.Gson;
57 import com.google.gson.JsonSyntaxException;
60 * The {@link SleepIQImpl} class handles all interactions with the sleepiq service.
62 * @author Gregory Moyer - Initial contribution
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";
69 private static final Gson GSON = GsonGenerator.create(false);
71 private final Logger logger = LoggerFactory.getLogger(SleepIQImpl.class);
73 private final HttpClient httpClient;
74 private final CookieStore cookieStore;
76 protected final Configuration config;
78 private final LoginRequest loginRequest = new LoginRequest();
79 private volatile @Nullable LoginInfo loginInfo;
81 public SleepIQImpl(Configuration config, HttpClient httpClient) {
83 this.httpClient = httpClient;
84 cookieStore = httpClient.getCookieStore();
85 loginRequest.setLogin(config.getUsername());
86 loginRequest.setPassword(config.getPassword());
90 public void shutdown() {
91 cookieStore.removeAll();
95 public @Nullable LoginInfo login() throws LoginException, UnauthorizedException {
96 logger.trace("SleepIQ: login: loginInfo={}", loginInfo);
97 if (loginInfo == null) {
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());
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);
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);
121 loginInfo = GSON.fromJson(response.getContentAsString(), LoginInfo.class);
122 } catch (JsonSyntaxException e) {
123 throw new LoginException("Failed to parse 'login' response");
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");
136 public List<Bed> getBeds() throws LoginException, CommunicationException, ResponseFormatException {
138 String contentResponse = cloudRequest(Endpoints.bed());
139 BedsResponse response = GSON.fromJson(contentResponse, BedsResponse.class);
140 if (response != null) {
141 return response.getBeds();
143 throw new ResponseFormatException("Failed to get a valid 'beds' response from cloud");
145 } catch (JsonSyntaxException e) {
146 throw new ResponseFormatException("Failed to parse 'beds' response");
151 public FamilyStatusResponse getFamilyStatus()
152 throws LoginException, ResponseFormatException, CommunicationException {
154 String contentResponse = cloudRequest(Endpoints.familyStatus());
155 FamilyStatusResponse response = GSON.fromJson(contentResponse, FamilyStatusResponse.class);
156 if (response != null) {
159 throw new ResponseFormatException("Failed to get a valid 'familyStatus' response from cloud");
161 } catch (JsonSyntaxException e) {
162 throw new ResponseFormatException("Failed to parse 'familyStatus' response");
167 public List<Sleeper> getSleepers() throws LoginException, ResponseFormatException, CommunicationException {
169 String contentResponse = cloudRequest(Endpoints.sleeper());
170 SleepersResponse response = GSON.fromJson(contentResponse, SleepersResponse.class);
171 if (response != null) {
172 return response.getSleepers();
174 throw new ResponseFormatException("Failed to get a valid 'sleepers' response from cloud");
176 } catch (JsonSyntaxException e) {
177 throw new ResponseFormatException("Failed to parse 'sleepers' response");
182 public PauseModeResponse getPauseMode(String bedId)
183 throws LoginException, ResponseFormatException, CommunicationException {
185 String contentResponse = cloudRequest(Endpoints.pauseMode(bedId));
186 PauseModeResponse response = GSON.fromJson(contentResponse, PauseModeResponse.class);
187 if (response != null) {
190 throw new ResponseFormatException("Failed to get a valid 'pauseMode' response from cloud");
192 } catch (JsonSyntaxException e) {
193 throw new ResponseFormatException("Failed to parse 'pauseMode' response");
198 public SleepDataResponse getSleepData(String sleeperId, SleepDataInterval interval)
199 throws LoginException, ResponseFormatException, CommunicationException {
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) {
211 throw new ResponseFormatException("Failed to get a valid 'sleepData' response from cloud");
213 } catch (JsonSyntaxException e) {
214 throw new ResponseFormatException("Failed to parse 'sleepData' response");
218 private String formatSleepDataDate(ZonedDateTime zonedDateTime) {
219 return DateTimeFormatter.ofPattern("yyyy-MM-dd").format(zonedDateTime);
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);
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, "");
240 private String cloudRequest(String endpoint)
241 throws LoginException, ResponseFormatException, CommunicationException {
242 return cloudRequest(endpoint, null, null);
245 private String cloudRequest(String endpoint, Map<String, String> parameters)
246 throws LoginException, ResponseFormatException, CommunicationException {
247 return cloudRequest(endpoint, parameters, null);
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
258 response = (body == null ? doGet(endpoint, parameters) : doPut(endpoint, parameters, body));
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()));
264 return response.getContentAsString();
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);
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);
285 private ContentResponse doRequest(Request request, @Nullable Map<String, String> parameters)
286 throws CommunicationException {
288 if (parameters != null) {
289 for (String key : parameters.keySet()) {
290 request.param(key, parameters.get(key));
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());
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());
304 private void addCookiesToRequest(Request request) {
305 cookieStore.get(config.getBaseUri()).forEach(cookie -> {
306 request.cookie(cookie);
310 private boolean isUnauthorized(ContentResponse response) {
311 return response.getStatus() == HttpStatus.Code.UNAUTHORIZED.getCode();
314 private boolean isNotOk(ContentResponse response) {
315 return response.getStatus() != HttpStatus.Code.OK.getCode();
318 private synchronized void resetLogin() {
319 logger.debug("SleepIQ: resetLogin: Set loginInfo=null to force login on next transaction");