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.iaqualink.internal.api;
15 import java.io.IOException;
16 import java.lang.reflect.Type;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.Objects;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeoutException;
24 import javax.ws.rs.core.UriBuilder;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
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.iaqualink.internal.api.dto.AccountInfo;
35 import org.openhab.binding.iaqualink.internal.api.dto.Auxiliary;
36 import org.openhab.binding.iaqualink.internal.api.dto.Device;
37 import org.openhab.binding.iaqualink.internal.api.dto.Home;
38 import org.openhab.binding.iaqualink.internal.api.dto.OneTouch;
39 import org.openhab.binding.iaqualink.internal.api.dto.SignIn;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.FieldNamingPolicy;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonDeserializationContext;
48 import com.google.gson.JsonDeserializer;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParseException;
52 import com.google.gson.JsonPrimitive;
55 * IAqualink HTTP Client
57 * The {@link IAqualinkClient} provides basic HTTP commands to control and monitor an iAquaLink
60 * GSON is used to provide custom deserialization on the JSON results. These results
61 * unfortunately are not returned as normalized JSON objects and require complex deserialization
64 * @author Dan Cunningham - Initial contribution
68 public class IAqualinkClient {
69 private final Logger logger = LoggerFactory.getLogger(IAqualinkClient.class);
71 private static final String HEADER_AGENT = "iAqualink/98 CFNetwork/978.0.7 Darwin/18.6.0";
72 private static final String HEADER_ACCEPT = "*/*";
73 private static final String HEADER_ACCEPT_LANGUAGE = "en-us";
74 private static final String HEADER_ACCEPT_ENCODING = "br, gzip, deflate";
76 private static final String AUTH_URL = "https://prod.zodiac-io.com/users/v1/login";
77 private static final String DEVICES_URL = "https://r-api.iaqualink.net/devices.json";
78 private static final String IAQUALINK_BASE_URL = "https://p-api.iaqualink.net/v1/mobile/session.json";
80 private Gson gson = new GsonBuilder().registerTypeAdapter(Home.class, new HomeDeserializer())
81 .registerTypeAdapter(OneTouch[].class, new OneTouchDeserializer())
82 .registerTypeAdapter(Auxiliary[].class, new AuxDeserializer())
83 .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
84 private Gson gsonInternal = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
86 private HttpClient httpClient;
88 @SuppressWarnings("serial")
89 public class NotAuthorizedException extends Exception {
90 public NotAuthorizedException(String message) {
99 public IAqualinkClient(HttpClient httpClient) {
100 this.httpClient = httpClient;
104 * Initial login to service
110 * @throws IOException
111 * @throws NotAuthorizedException
113 public AccountInfo login(@Nullable String username, @Nullable String password, @Nullable String apiKey)
114 throws IOException, NotAuthorizedException {
115 String signIn = gson.toJson(new SignIn(apiKey, username, password)).toString();
117 ContentResponse response = httpClient.newRequest(AUTH_URL).method(HttpMethod.POST)
118 .content(new StringContentProvider(signIn), "application/json").send();
119 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
120 throw new NotAuthorizedException(response.getReason());
122 if (response.getStatus() != HttpStatus.OK_200) {
123 throw new IOException(response.getReason());
125 return Objects.requireNonNull(gson.fromJson(response.getContentAsString(), AccountInfo.class));
126 } catch (InterruptedException | TimeoutException | ExecutionException e) {
127 throw new IOException(e);
132 * List all devices (pools) registered to an account
137 * @return {@link Device[]}
138 * @throws IOException
139 * @throws NotAuthorizedException
141 public Device[] getDevices(@Nullable String apiKey, @Nullable String token, int id)
142 throws IOException, NotAuthorizedException {
143 return getAqualinkObject(UriBuilder.fromUri(DEVICES_URL). //
144 queryParam("api_key", apiKey). //
145 queryParam("authentication_token", token). //
146 queryParam("user_id", id).build(), Device[].class);
150 * Retrieves the HomeScreen
152 * @param serialNumber
154 * @return {@link Home}
155 * @throws IOException
156 * @throws NotAuthorizedException
158 public Home getHome(@Nullable String serialNumber, @Nullable String sessionId)
159 throws IOException, NotAuthorizedException {
160 return homeScreenCommand(serialNumber, sessionId, "get_home");
164 * Retrieves {@link OneTouch[]} macros
166 * @param serialNumber
168 * @return {@link OneTouch[]}
169 * @throws IOException
170 * @throws NotAuthorizedException
172 public OneTouch[] getOneTouch(@Nullable String serialNumber, @Nullable String sessionId)
173 throws IOException, NotAuthorizedException {
174 return oneTouchCommand(serialNumber, sessionId, "get_onetouch");
178 * Retrieves {@link Auxiliary[]} devices
182 * @return {@link Auxiliary[]}
183 * @throws IOException
184 * @throws NotAuthorizedException
186 public Auxiliary[] getAux(@Nullable String serial, @Nullable String sessionID)
187 throws IOException, NotAuthorizedException {
188 return auxCommand(serial, sessionID, "get_devices");
192 * Sends a HomeScreen Set command
196 * @param homeElementID
198 * @throws IOException
199 * @throws NotAuthorizedException
201 public Home homeScreenSetCommand(@Nullable String serial, @Nullable String sessionID, String homeElementID)
202 throws IOException, NotAuthorizedException {
203 return homeScreenCommand(serial, sessionID, "set_" + homeElementID);
207 * Sends an Auxiliary Set command
213 * @throws IOException
214 * @throws NotAuthorizedException
216 public Auxiliary[] auxSetCommand(@Nullable String serial, @Nullable String sessionID, String auxID)
217 throws IOException, NotAuthorizedException {
218 return auxCommand(serial, sessionID, "set_" + auxID);
222 * Sends an Auxiliary light command
230 * @throws IOException
231 * @throws NotAuthorizedException
233 public Auxiliary[] lightCommand(@Nullable String serial, @Nullable String sessionID, String auxID,
234 String lightValue, String subType) throws IOException, NotAuthorizedException {
235 return getAqualinkObject(baseURI(). //
236 queryParam("aux", auxID). //
237 queryParam("command", "set_light"). //
238 queryParam("light", lightValue). //
239 queryParam("serial", serial). //
240 queryParam("subtype", subType). //
241 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
245 * Sends an Auxiliary dimmer command
247 * @param serialNumber
252 * @throws IOException
253 * @throws NotAuthorizedException
255 public Auxiliary[] dimmerCommand(@Nullable String serial, @Nullable String sessionID, String auxID, String level)
256 throws IOException, NotAuthorizedException {
257 return getAqualinkObject(baseURI().queryParam("aux", auxID). //
258 queryParam("command", "set_dimmer"). //
259 queryParam("level", level). //
260 queryParam("serial", serial). //
261 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
265 * Sets the Spa Temperature Setpoint
271 * @throws IOException
272 * @throws NotAuthorizedException
274 public Home setSpaTemp(@Nullable String serial, @Nullable String sessionID, float spaSetpoint)
275 throws IOException, NotAuthorizedException {
276 return getAqualinkObject(baseURI(). //
277 queryParam("command", "set_temps"). //
278 queryParam("temp1", spaSetpoint). //
279 queryParam("serial", serial). //
280 queryParam("sessionID", sessionID).build(), Home.class);
284 * Sets the Pool Temperature Setpoint
288 * @param poolSetpoint
290 * @throws IOException
291 * @throws NotAuthorizedException
293 public Home setPoolTemp(@Nullable String serial, @Nullable String sessionID, float poolSetpoint)
294 throws IOException, NotAuthorizedException {
295 return getAqualinkObject(baseURI(). //
296 queryParam("command", "set_temps"). //
297 queryParam("temp2", poolSetpoint). //
298 queryParam("serial", serial). //
299 queryParam("sessionID", sessionID).build(), Home.class);
303 * Sends a OneTouch set command
309 * @throws IOException
310 * @throws NotAuthorizedException
312 public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
313 throws IOException, NotAuthorizedException {
314 return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
317 private Home homeScreenCommand(@Nullable String serial, @Nullable String sessionID, String command)
318 throws IOException, NotAuthorizedException {
319 return getAqualinkObject(baseURI().queryParam("command", command). //
320 queryParam("serial", serial). //
321 queryParam("sessionID", sessionID).build(), Home.class);
324 private Auxiliary[] auxCommand(@Nullable String serial, @Nullable String sessionID, String command)
325 throws IOException, NotAuthorizedException {
326 return getAqualinkObject(baseURI(). //
327 queryParam("command", command). //
328 queryParam("serial", serial). //
329 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
332 private OneTouch[] oneTouchCommand(@Nullable String serial, @Nullable String sessionID, String command)
333 throws IOException, NotAuthorizedException {
334 return getAqualinkObject(baseURI().queryParam("command", command). //
335 queryParam("serial", serial). //
336 queryParam("sessionID", sessionID).build(), OneTouch[].class);
339 private UriBuilder baseURI() {
340 return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
349 * @throws IOException
350 * @throws NotAuthorizedException
352 private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
353 return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
360 * @throws IOException
361 * @throws NotAuthorizedException
363 private String getRequest(URI uri) throws IOException, NotAuthorizedException {
365 logger.trace("Trying {}", uri);
366 ContentResponse response = httpClient.newRequest(uri).method(HttpMethod.GET) //
367 .agent(HEADER_AGENT) //
368 .header(HttpHeader.ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE) //
369 .header(HttpHeader.ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING) //
370 .header(HttpHeader.ACCEPT, HEADER_ACCEPT) //
372 logger.trace("Response {}", response);
373 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
374 throw new NotAuthorizedException(response.getReason());
376 if (response.getStatus() != HttpStatus.OK_200) {
377 throw new IOException(response.getReason());
379 return response.getContentAsString();
380 } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
381 throw new IOException(e);
385 /////////////// .........Here be dragons...../////////////////////////
387 class HomeDeserializer implements JsonDeserializer<Home> {
389 public @Nullable Home deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
390 throws JsonParseException {
391 JsonObject jsonObject = json.getAsJsonObject();
392 JsonArray homeScreen = jsonObject.getAsJsonArray("home_screen");
393 JsonObject home = new JsonObject();
394 JsonObject serializedMap = new JsonObject();
395 if (homeScreen != null) {
396 homeScreen.forEach(element -> {
397 element.getAsJsonObject().entrySet().forEach(entry -> {
398 JsonElement value = entry.getValue();
399 home.add(entry.getKey(), value);
400 if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
401 serializedMap.add(entry.getKey(), value);
405 home.add("serialized_map", serializedMap);
406 return gsonInternal.fromJson(home, Home.class);
408 throw new JsonParseException("Invalid structure for Home class");
412 class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
414 public OneTouch @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
415 throws JsonParseException {
416 JsonObject jsonObject = json.getAsJsonObject();
417 JsonArray oneTouchScreen = jsonObject.getAsJsonArray("onetouch_screen");
418 List<OneTouch> list = new ArrayList<>();
419 if (oneTouchScreen != null) {
420 oneTouchScreen.forEach(oneTouchScreenElement -> {
421 oneTouchScreenElement.getAsJsonObject().entrySet().forEach(oneTouchScreenEntry -> {
422 if (oneTouchScreenEntry.getKey().startsWith("onetouch_")) {
423 JsonArray oneTouchArray = oneTouchScreenEntry.getValue().getAsJsonArray();
424 if (oneTouchArray != null) {
425 JsonObject oneTouchJson = new JsonObject();
426 oneTouchJson.add("name", new JsonPrimitive(oneTouchScreenEntry.getKey()));
427 oneTouchArray.forEach(arrayElement -> {
428 arrayElement.getAsJsonObject().entrySet().forEach(oneTouchEntry -> {
429 oneTouchJson.add(oneTouchEntry.getKey(), oneTouchEntry.getValue());
432 list.add(Objects.requireNonNull(gsonInternal.fromJson(oneTouchJson, OneTouch.class)));
438 return list.toArray(new OneTouch[list.size()]);
442 class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
444 public Auxiliary @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
445 throws JsonParseException {
446 JsonObject jsonObject = json.getAsJsonObject();
447 JsonArray auxScreen = jsonObject.getAsJsonArray("devices_screen");
448 List<Auxiliary> list = new ArrayList<>();
449 if (auxScreen != null) {
450 auxScreen.forEach(auxElement -> {
451 auxElement.getAsJsonObject().entrySet().forEach(auxScreenEntry -> {
452 if (auxScreenEntry.getKey().startsWith("aux_")) {
453 JsonArray auxArray = auxScreenEntry.getValue().getAsJsonArray();
454 if (auxArray != null) {
455 JsonObject auxJson = new JsonObject();
456 auxJson.add("name", new JsonPrimitive(auxScreenEntry.getKey()));
457 auxArray.forEach(arrayElement -> {
458 arrayElement.getAsJsonObject().entrySet().forEach(auxEntry -> {
459 auxJson.add(auxEntry.getKey(), auxEntry.getValue());
462 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
468 return list.toArray(new Auxiliary[list.size()]);