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
180 * @param serialNumber
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
224 * @param serialNumber
229 * @throws IOException
230 * @throws NotAuthorizedException
232 public Auxiliary[] lightCommand(@Nullable String serial, @Nullable String sessionID, String auxID,
233 String lightValue, String subType) throws IOException, NotAuthorizedException {
234 return getAqualinkObject(baseURI(). //
235 queryParam("aux", auxID). //
236 queryParam("command", "set_light"). //
237 queryParam("light", lightValue). //
238 queryParam("serial", serial). //
239 queryParam("subtype", subType). //
240 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
244 * Sends an Auxiliary dimmer command
246 * @param serialNumber
251 * @throws IOException
252 * @throws NotAuthorizedException
254 public Auxiliary[] dimmerCommand(@Nullable String serial, @Nullable String sessionID, String auxID, String level)
255 throws IOException, NotAuthorizedException {
256 return getAqualinkObject(baseURI().queryParam("aux", auxID). //
257 queryParam("command", "set_dimmer"). //
258 queryParam("level", level). //
259 queryParam("serial", serial). //
260 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
264 * Sets the Spa Temperature Setpoint
266 * @param serialNumber
270 * @throws IOException
271 * @throws NotAuthorizedException
273 public Home setSpaTemp(@Nullable String serial, @Nullable String sessionID, float spaSetpoint)
274 throws IOException, NotAuthorizedException {
275 return getAqualinkObject(baseURI(). //
276 queryParam("command", "set_temps"). //
277 queryParam("temp1", spaSetpoint). //
278 queryParam("serial", serial). //
279 queryParam("sessionID", sessionID).build(), Home.class);
283 * Sets the Pool Temperature Setpoint
285 * @param serialNumber
287 * @param poolSetpoint
289 * @throws IOException
290 * @throws NotAuthorizedException
292 public Home setPoolTemp(@Nullable String serial, @Nullable String sessionID, float poolSetpoint)
293 throws IOException, NotAuthorizedException {
294 return getAqualinkObject(baseURI(). //
295 queryParam("command", "set_temps"). //
296 queryParam("temp2", poolSetpoint). //
297 queryParam("serial", serial). //
298 queryParam("sessionID", sessionID).build(), Home.class);
302 * Sends a OneTouch set command
308 * @throws IOException
309 * @throws NotAuthorizedException
311 public OneTouch[] oneTouchSetCommand(@Nullable String serial, @Nullable String sessionID, String oneTouchID)
312 throws IOException, NotAuthorizedException {
313 return oneTouchCommand(serial, sessionID, "set_" + oneTouchID);
316 private Home homeScreenCommand(@Nullable String serial, @Nullable String sessionID, String command)
317 throws IOException, NotAuthorizedException {
318 return getAqualinkObject(baseURI().queryParam("command", command). //
319 queryParam("serial", serial). //
320 queryParam("sessionID", sessionID).build(), Home.class);
323 private Auxiliary[] auxCommand(@Nullable String serial, @Nullable String sessionID, String command)
324 throws IOException, NotAuthorizedException {
325 return getAqualinkObject(baseURI(). //
326 queryParam("command", command). //
327 queryParam("serial", serial). //
328 queryParam("sessionID", sessionID).build(), Auxiliary[].class);
331 private OneTouch[] oneTouchCommand(@Nullable String serial, @Nullable String sessionID, String command)
332 throws IOException, NotAuthorizedException {
333 return getAqualinkObject(baseURI().queryParam("command", command). //
334 queryParam("serial", serial). //
335 queryParam("sessionID", sessionID).build(), OneTouch[].class);
338 private UriBuilder baseURI() {
339 return UriBuilder.fromUri(IAQUALINK_BASE_URL).queryParam("actionID", "command");
348 * @throws IOException
349 * @throws NotAuthorizedException
351 private <T> T getAqualinkObject(URI uri, Type typeOfT) throws IOException, NotAuthorizedException {
352 return Objects.requireNonNull(gson.fromJson(getRequest(uri), typeOfT));
359 * @throws IOException
360 * @throws NotAuthorizedException
362 private String getRequest(URI uri) throws IOException, NotAuthorizedException {
364 logger.trace("Trying {}", uri);
365 ContentResponse response = httpClient.newRequest(uri).method(HttpMethod.GET) //
366 .agent(HEADER_AGENT) //
367 .header(HttpHeader.ACCEPT_LANGUAGE, HEADER_ACCEPT_LANGUAGE) //
368 .header(HttpHeader.ACCEPT_ENCODING, HEADER_ACCEPT_ENCODING) //
369 .header(HttpHeader.ACCEPT, HEADER_ACCEPT) //
371 logger.trace("Response {}", response);
372 if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) {
373 throw new NotAuthorizedException(response.getReason());
375 if (response.getStatus() != HttpStatus.OK_200) {
376 throw new IOException(response.getReason());
378 return response.getContentAsString();
379 } catch (InterruptedException | TimeoutException | ExecutionException | JsonParseException e) {
380 throw new IOException(e);
384 /////////////// .........Here be dragons...../////////////////////////
386 class HomeDeserializer implements JsonDeserializer<Home> {
388 public @Nullable Home deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
389 throws JsonParseException {
390 JsonObject jsonObject = json.getAsJsonObject();
391 JsonArray homeScreen = jsonObject.getAsJsonArray("home_screen");
392 JsonObject home = new JsonObject();
393 JsonObject serializedMap = new JsonObject();
394 if (homeScreen != null) {
395 homeScreen.forEach(element -> {
396 element.getAsJsonObject().entrySet().forEach(entry -> {
397 JsonElement value = entry.getValue();
398 home.add(entry.getKey(), value);
399 if (value.isJsonPrimitive() && value.getAsJsonPrimitive().isString()) {
400 serializedMap.add(entry.getKey(), value);
404 home.add("serialized_map", serializedMap);
405 return gsonInternal.fromJson(home, Home.class);
407 throw new JsonParseException("Invalid structure for Home class");
411 class OneTouchDeserializer implements JsonDeserializer<OneTouch[]> {
413 public OneTouch @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
414 throws JsonParseException {
415 JsonObject jsonObject = json.getAsJsonObject();
416 JsonArray oneTouchScreen = jsonObject.getAsJsonArray("onetouch_screen");
417 List<OneTouch> list = new ArrayList<>();
418 if (oneTouchScreen != null) {
419 oneTouchScreen.forEach(oneTouchScreenElement -> {
420 oneTouchScreenElement.getAsJsonObject().entrySet().forEach(oneTouchScreenEntry -> {
421 if (oneTouchScreenEntry.getKey().startsWith("onetouch_")) {
422 JsonArray oneTouchArray = oneTouchScreenEntry.getValue().getAsJsonArray();
423 if (oneTouchArray != null) {
424 JsonObject oneTouchJson = new JsonObject();
425 oneTouchJson.add("name", new JsonPrimitive(oneTouchScreenEntry.getKey()));
426 oneTouchArray.forEach(arrayElement -> {
427 arrayElement.getAsJsonObject().entrySet().forEach(oneTouchEntry -> {
428 oneTouchJson.add(oneTouchEntry.getKey(), oneTouchEntry.getValue());
431 list.add(Objects.requireNonNull(gsonInternal.fromJson(oneTouchJson, OneTouch.class)));
437 return list.toArray(new OneTouch[list.size()]);
441 class AuxDeserializer implements JsonDeserializer<Auxiliary[]> {
443 public Auxiliary @Nullable [] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
444 throws JsonParseException {
445 JsonObject jsonObject = json.getAsJsonObject();
446 JsonArray auxScreen = jsonObject.getAsJsonArray("devices_screen");
447 List<Auxiliary> list = new ArrayList<>();
448 if (auxScreen != null) {
449 auxScreen.forEach(auxElement -> {
450 auxElement.getAsJsonObject().entrySet().forEach(auxScreenEntry -> {
451 if (auxScreenEntry.getKey().startsWith("aux_")) {
452 JsonArray auxArray = auxScreenEntry.getValue().getAsJsonArray();
453 if (auxArray != null) {
454 JsonObject auxJson = new JsonObject();
455 auxJson.add("name", new JsonPrimitive(auxScreenEntry.getKey()));
456 auxArray.forEach(arrayElement -> {
457 arrayElement.getAsJsonObject().entrySet().forEach(auxEntry -> {
458 auxJson.add(auxEntry.getKey(), auxEntry.getValue());
461 list.add(Objects.requireNonNull(gsonInternal.fromJson(auxJson, Auxiliary.class)));
467 return list.toArray(new Auxiliary[list.size()]);