]> git.basschouten.com Git - openhab-addons.git/commitdiff
[icloud] Rework authentication to reflect changes in iCloud API (#13691)
authorSimon Spielmann <simon.spielmann@gmx.de>
Thu, 15 Dec 2022 08:18:11 +0000 (09:18 +0100)
committerGitHub <noreply@github.com>
Thu, 15 Dec 2022 08:18:11 +0000 (09:18 +0100)
* Implement Authentication (WIP)
* Validation Code accepted
* Refactor session state
* RefreshClient working
* Implement session persistence in openhab store
* Integration in binding
* Remove persistent cookies, which break authentication
* Bugfixing
* Add code configuration to UI
* Improve documentation, error-handling and cleanup
* Rework auth order
* Rework auth process
* Add 2-FA-auth to documentation
* Set bridge to online if data refresh works
* Case-sensitive rename ICloudAPIResponseException
* Include authentication in refresh flow
* Fix regression for data not being updated
* Fix typo in i18n props
* Fix review and checkstyle.
* More javadoc, new RetryException
* Introduce @NonNullByDefault
* Introduce server for RetryException, add NonNullbyDefault, fix warnings
* Rework for contribution, e.g. null checks, ...
* Fix checkstyle
* Move JsonUtils to utilities package
* Async initialize bridge handler.
* Report Device OFFLINE if Bridge is OFFLINE
* Set bridge thing status to UNKOWN in init
* Move refresh init into async init
* Cancel init task in dispose

Also-by: Leo Siepel <leosiepel@gmail.com>
Signed-off-by: Simon Spielmann <simon.spielmann@gmx.de>
40 files changed:
bundles/org.openhab.binding.icloud/README.md
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/FindMyIPhoneServiceManager.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudApiResponseException.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudBindingConstants.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudConnection.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudDeviceInformationListener.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudDeviceInformationParser.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudHandlerFactory.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudService.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudSession.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/RetryException.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/configuration/ICloudAccountThingConfiguration.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/discovery/ICloudDeviceDiscovery.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/AuthState.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudAccountBridgeHandler.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudDeviceHandler.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountDataResponse.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountUserInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceFeatures.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceLocation.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContext.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContextTimezone.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudAccountDataRequest.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudFindMyDeviceRequest.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountDataResponse.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountUserInfo.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceFeatures.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceInformation.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceLocation.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContext.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContextTimezone.java [deleted file]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/CustomCookieStore.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/ICloudTextTranslator.java
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/JsonUtils.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/ListUtil.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/Pair.java [new file with mode: 0644]
bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/i18n/iCloud.properties
bundles/org.openhab.binding.icloud/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java [new file with mode: 0644]

index aa2216783a25eeb57f096a12e6b51c521d731a11..8a8ad621ebd1ef26c9dce8ec130a04ddda3f58e9 100644 (file)
@@ -29,6 +29,12 @@ The account Thing, more precisely the account Bridge, represents one Apple iClou
 The account can be connected to multiple Apple devices which are represented as Things below the Bridge, see the example below.
 You may create multiple account Things for multiple accounts.
 
+If your Apple account has 2-factor-authentication enabled configuration requires two steps.
+First start by adding the Apple ID and password to your account thing configuration.
+You will receive a notification with a code on one of your Apple devices then.
+Add this code to the code parameter of the thing then and wait.
+The binding should be reinitialized and perform the authentication.
+
 ### Device Thing
 
 A device is identified by the device ID provided by Apple.
@@ -62,7 +68,7 @@ The following channels are available (if supported by the device):
 ### icloud.things
 
 ```php
-Bridge icloud:account:myaccount [appleId="mail@example.com", password="secure", refreshTimeInMinutes=5]
+Bridge icloud:account:myaccount [appleId="mail@example.com", password="secure", code="123456", refreshTimeInMinutes=5]
 {
     Thing device myiPhone8 "iPhone 8" @ "World" [deviceId="VIRG9FsrvXfE90ewVBA1H5swtwEQePdXVjHq3Si6pdJY2Cjro8QlreHYVGSUzuWV"]
 }
@@ -76,14 +82,14 @@ The information _@ "World"_ is optional.
 ```php
 Group    iCloud_Group "iPhone"
 
-String   iPhone_BatteryStatus             "Battery Status [%s]" <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryStatus"}
-Number   iPhone_BatteryLevel              "Battery Level [%d %%]"   <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryLevel"}
-Switch   iPhone_FindMyPhone               "Trigger Find My iPhone"           (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:findMyPhone", autoupdate="false"}
-Switch   iPhone_Refresh                   "Force iPhone Refresh"             (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location", autoupdate="false"}
-Location iPhone_Location                  "Coordinates"                      (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location"}
-Number   iPhone_LocationAccuracy          "Coordinates Accuracy [%.0f m]"    (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationAccuracy"}
+String   iPhone_BatteryStatus             "Battery Status [%s]"     <battery>   (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryStatus"}
+Number   iPhone_BatteryLevel              "Battery Level [%d %%]"   <battery>   (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryLevel"}
+Switch   iPhone_FindMyPhone               "Trigger Find My iPhone"              (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:findMyPhone", autoupdate="false"}
+Switch   iPhone_Refresh                   "Force iPhone Refresh"                (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location", autoupdate="false"}
+Location iPhone_Location                  "Coordinates"                         (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location"}
+Number   iPhone_LocationAccuracy          "Coordinates Accuracy [%.0f m]"       (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationAccuracy"}
 DateTime iPhone_LocationLastUpdate        "Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <time> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationLastUpdate"}
-Switch   iPhone_Home                      "Phone Home"            <presence> (iCloud_Group)
+Switch   iPhone_Home                      "Phone Home"              <presence>  (iCloud_Group)
 ```
 
 ### icloud.sitemap
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/FindMyIPhoneServiceManager.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/FindMyIPhoneServiceManager.java
new file mode 100644 (file)
index 0000000..89cf08e
--- /dev/null
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.icloud.internal.utilities.JsonUtils;
+
+/**
+ * This class gives access to the find my iPhone (FMIP) service.
+ *
+ * @author Simon Spielmann - Initial Contribution.
+ */
+@NonNullByDefault
+public class FindMyIPhoneServiceManager {
+
+    private ICloudSession session;
+
+    private URI fmipRefreshUrl;
+
+    private URI fmipSoundUrl;
+
+    private static final String FMIP_ENDPOINT = "/fmipservice/client/web";
+
+    /**
+     * The constructor.
+     *
+     * @param session {@link ICloudSession} to use for API calls.
+     * @param serviceRoot Root URL for FMIP service.
+     */
+    public FindMyIPhoneServiceManager(ICloudSession session, String serviceRoot) {
+        this.session = session;
+        this.fmipRefreshUrl = URI.create(serviceRoot + FMIP_ENDPOINT + "/refreshClient");
+        this.fmipSoundUrl = URI.create(serviceRoot + FMIP_ENDPOINT + "/playSound");
+    }
+
+    /**
+     * Receive client information as JSON.
+     *
+     * @return Information about all clients as JSON
+     *         {@link org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation}.
+     *
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this blocking request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     *
+     */
+    public String refreshClient() throws IOException, InterruptedException, ICloudApiResponseException {
+        Map<String, Object> request = Map.of("clientContext",
+                Map.of("fmly", true, "shouldLocate", true, "selectedDevice", "All", "deviceListVersion", 1));
+        return session.post(this.fmipRefreshUrl.toString(), JsonUtils.toJson(request), null);
+    }
+
+    /**
+     * Play sound (find my iPhone) on given device.
+     *
+     * @param deviceId ID of the device to play sound on
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this blocking request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     */
+    public void playSound(String deviceId) throws IOException, InterruptedException, ICloudApiResponseException {
+        Map<String, Object> request = Map.of("device", deviceId, "fmyl", true, "subject", "Message from openHAB.");
+        session.post(this.fmipSoundUrl.toString(), JsonUtils.toJson(request), null);
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudApiResponseException.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudApiResponseException.java
new file mode 100644 (file)
index 0000000..c3c9cbc
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ *
+ * Exception for errors during calls of the iCloud API.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+@NonNullByDefault
+public class ICloudApiResponseException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+    private int statusCode;
+
+    /**
+     * The constructor.
+     *
+     * @param url URL for which the exception occurred
+     * @param statusCode HTTP status code which was reported
+     */
+    public ICloudApiResponseException(String url, int statusCode) {
+        super(String.format("Request %s failed with %s.", url, statusCode));
+        this.statusCode = statusCode;
+    }
+
+    /**
+     * @return statusCode HTTP status code of failed request.
+     */
+    public int getStatusCode() {
+        return this.statusCode;
+    }
+}
index 5a94cc9aeda49f9db7648e8febad7fc5c7eaef80..9aebc26b1bb28d7073aee09b63cfffdf82866920 100644 (file)
@@ -24,9 +24,8 @@ import org.openhab.core.thing.ThingTypeUID;
  * used across the whole binding.
  *
  * @author Patrik Gfeller - Initial contribution
- * @author Patrik Gfeller
- *         - Class renamed to be more consistent
- *         - Constant FIND_MY_DEVICE_REQUEST_SUBJECT introduced
+ * @author Patrik Gfeller - Class renamed to be more consistent
+ * @author Patrik Gfeller - Constant FIND_MY_DEVICE_REQUEST_SUBJECT introduced
  * @author Gaël L'hopital - Added low battery
  */
 @NonNullByDefault
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudConnection.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudConnection.java
deleted file mode 100644 (file)
index 9c191c0..0000000
+++ /dev/null
@@ -1,93 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.time.Duration;
-import java.util.Base64;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.icloud.internal.json.request.ICloudAccountDataRequest;
-import org.openhab.binding.icloud.internal.json.request.ICloudFindMyDeviceRequest;
-import org.openhab.core.io.net.http.HttpRequestBuilder;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-
-/**
- * Handles communication with the Apple server. Provides methods to
- * get device information and to find a device.
- *
- * @author Patrik Gfeller - Initial Contribution
- * @author Patrik Gfeller - SOCKET_TIMEOUT changed from 2500 to 10000
- * @author Martin van Wingerden - add support for custom CA of https://fmipmobile.icloud.com
- */
-@NonNullByDefault
-public class ICloudConnection {
-    private static final String ICLOUD_URL = "https://www.icloud.com";
-    private static final String ICLOUD_API_BASE_URL = "https://fmipmobile.icloud.com";
-    private static final String ICLOUD_API_URL = ICLOUD_API_BASE_URL + "/fmipservice/device/";
-    private static final String ICLOUD_API_COMMAND_PING_DEVICE = "/playSound";
-    private static final String ICLOUD_API_COMMAND_REQUEST_DATA = "/initClient";
-    private static final int SOCKET_TIMEOUT = 15;
-
-    private final Gson gson = new GsonBuilder().create();
-    private final String iCloudDataRequest = gson.toJson(ICloudAccountDataRequest.defaultInstance());
-
-    private final String authorization;
-    private final String iCloudDataRequestURL;
-    private final String iCloudFindMyDeviceURL;
-
-    public ICloudConnection(String appleId, String password) throws URISyntaxException {
-        authorization = new String(Base64.getEncoder().encode((appleId + ":" + password).getBytes()), UTF_8);
-        iCloudDataRequestURL = new URI(ICLOUD_API_URL + appleId + ICLOUD_API_COMMAND_REQUEST_DATA).toASCIIString();
-        iCloudFindMyDeviceURL = new URI(ICLOUD_API_URL + appleId + ICLOUD_API_COMMAND_PING_DEVICE).toASCIIString();
-    }
-
-    /***
-     * Sends a "find my device" request.
-     *
-     * @throws IOException
-     */
-    public void findMyDevice(String id) throws IOException {
-        callApi(iCloudFindMyDeviceURL, gson.toJson(new ICloudFindMyDeviceRequest(id)));
-    }
-
-    public String requestDeviceStatusJSON() throws IOException {
-        return callApi(iCloudDataRequestURL, iCloudDataRequest);
-    }
-
-    private String callApi(String url, String payload) throws IOException {
-        // @formatter:off
-        return HttpRequestBuilder.postTo(url)
-            .withTimeout(Duration.ofSeconds(SOCKET_TIMEOUT))
-            .withHeader("Authorization", "Basic " + authorization)
-            .withHeader("User-Agent", "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)")
-            .withHeader("Origin", ICLOUD_URL)
-            .withHeader("charset", "utf-8")
-            .withHeader("Accept-language", "en-us")
-            .withHeader("Connection", "keep-alive")
-            .withHeader("X-Apple-Find-Api-Ver", "2.0")
-            .withHeader("X-Apple-Authscheme", "UserIdGuest")
-            .withHeader("X-Apple-Realm-Support", "1.0")
-            .withHeader("X-Client-Name", "iPad")
-            .withHeader("Content-Type", "application/json")
-            .withContent(payload)
-            .getContentAsString();
-        // @formatter:on
-    }
-}
index 5b28a434e0556be43c05e8f91a2eee6b655cae49..52b7f4cf4bb21cd1acf5a0c80520f396145b2416 100644 (file)
@@ -15,7 +15,7 @@ package org.openhab.binding.icloud.internal;
 import java.util.List;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
 
 /**
  * Classes that implement this interface are interested in device information updates.
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudDeviceInformationParser.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudDeviceInformationParser.java
deleted file mode 100644 (file)
index f6d5430..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.icloud.internal.json.response.ICloudAccountDataResponse;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonSyntaxException;
-
-/**
- * Extracts iCloud device information from a given JSON string
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-@NonNullByDefault
-public class ICloudDeviceInformationParser {
-    private final Gson gson = new GsonBuilder().create();
-
-    public @Nullable ICloudAccountDataResponse parse(String json) throws JsonSyntaxException {
-        return gson.fromJson(json, ICloudAccountDataResponse.class);
-    }
-}
index ad23eabf79b861decf19f05d059b3b6386cbbc7f..64ca9ded945fbc825bc63a8992f8de0d5065fa17 100644 (file)
@@ -18,6 +18,7 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Map;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery;
 import org.openhab.binding.icloud.internal.handler.ICloudAccountBridgeHandler;
@@ -25,6 +26,8 @@ import org.openhab.binding.icloud.internal.handler.ICloudDeviceHandler;
 import org.openhab.core.config.discovery.DiscoveryService;
 import org.openhab.core.i18n.LocaleProvider;
 import org.openhab.core.i18n.TranslationProvider;
+import org.openhab.core.storage.Storage;
+import org.openhab.core.storage.StorageService;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
@@ -33,21 +36,34 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.thing.binding.ThingHandlerFactory;
 import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
 
 /**
- * The {@link ICloudHandlerFactory} is responsible for creating things and thing
- * handlers.
+ * The {@link ICloudHandlerFactory} is responsible for creating things and thing handlers.
  *
  * @author Patrik Gfeller - Initial contribution
  */
 @Component(service = ThingHandlerFactory.class, configurationPid = "binding.icloud")
+@NonNullByDefault
 public class ICloudHandlerFactory extends BaseThingHandlerFactory {
     private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegistrations = new HashMap<>();
+
     private LocaleProvider localeProvider;
+
     private TranslationProvider i18nProvider;
 
+    private final StorageService storageService;
+
+    @Activate
+    public ICloudHandlerFactory(@Reference StorageService storageService, @Reference LocaleProvider localeProvider,
+            @Reference TranslationProvider i18nProvider) {
+        this.storageService = storageService;
+        this.localeProvider = localeProvider;
+        this.i18nProvider = i18nProvider;
+    }
+
     @Override
     public boolean supportsThingType(ThingTypeUID thingTypeUID) {
         return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
@@ -58,7 +74,9 @@ public class ICloudHandlerFactory extends BaseThingHandlerFactory {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (thingTypeUID.equals(THING_TYPE_ICLOUD)) {
-            ICloudAccountBridgeHandler bridgeHandler = new ICloudAccountBridgeHandler((Bridge) thing);
+            Storage<String> storage = this.storageService.getStorage(thing.getUID().toString(),
+                    String.class.getClassLoader());
+            ICloudAccountBridgeHandler bridgeHandler = new ICloudAccountBridgeHandler((Bridge) thing, storage);
             registerDeviceDiscoveryService(bridgeHandler);
             return bridgeHandler;
         }
@@ -77,42 +95,24 @@ public class ICloudHandlerFactory extends BaseThingHandlerFactory {
     }
 
     private synchronized void registerDeviceDiscoveryService(ICloudAccountBridgeHandler bridgeHandler) {
-        ICloudDeviceDiscovery discoveryService = new ICloudDeviceDiscovery(bridgeHandler, bundleContext.getBundle(),
-                i18nProvider, localeProvider);
+        ICloudDeviceDiscovery discoveryService = new ICloudDeviceDiscovery(bridgeHandler,
+                this.bundleContext.getBundle(), this.i18nProvider, this.localeProvider);
         discoveryService.activate();
-        this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(),
-                bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
+        this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(), this.bundleContext
+                .registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
     }
 
     private synchronized void unregisterDeviceDiscoveryService(ICloudAccountBridgeHandler bridgeHandler) {
         ServiceRegistration<?> serviceRegistration = this.discoveryServiceRegistrations
                 .get(bridgeHandler.getThing().getUID());
         if (serviceRegistration != null) {
-            ICloudDeviceDiscovery discoveryService = (ICloudDeviceDiscovery) bundleContext
+            ICloudDeviceDiscovery discoveryService = (ICloudDeviceDiscovery) this.bundleContext
                     .getService(serviceRegistration.getReference());
             if (discoveryService != null) {
                 discoveryService.deactivate();
             }
             serviceRegistration.unregister();
-            discoveryServiceRegistrations.remove(bridgeHandler.getThing().getUID());
+            this.discoveryServiceRegistrations.remove(bridgeHandler.getThing().getUID());
         }
     }
-
-    @Reference
-    protected void setLocaleProvider(LocaleProvider localeProvider) {
-        this.localeProvider = localeProvider;
-    }
-
-    protected void unsetLocaleProvider(LocaleProvider localeProvider) {
-        this.localeProvider = null;
-    }
-
-    @Reference
-    public void setTranslationProvider(TranslationProvider i18nProvider) {
-        this.i18nProvider = i18nProvider;
-    }
-
-    public void unsetTranslationProvider(TranslationProvider i18nProvider) {
-        this.i18nProvider = null;
-    }
 }
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudService.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudService.java
new file mode 100644 (file)
index 0000000..0ac0948
--- /dev/null
@@ -0,0 +1,329 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.icloud.internal.utilities.JsonUtils;
+import org.openhab.binding.icloud.internal.utilities.ListUtil;
+import org.openhab.binding.icloud.internal.utilities.Pair;
+import org.openhab.core.storage.Storage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * Class to access Apple iCloud API.
+ *
+ * The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+@NonNullByDefault
+public class ICloudService {
+
+    /**
+     *
+     */
+    private static final String ICLOUD_CLIENT_ID = "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d";
+
+    private final Logger logger = LoggerFactory.getLogger(ICloudService.class);
+
+    private static final String AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth";
+
+    private static final String HOME_ENDPOINT = "https://www.icloud.com";
+
+    private static final String SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1";
+
+    private String appleId;
+
+    private String password;
+
+    private String clientId;
+
+    private Map<String, Object> data = new HashMap<>();
+
+    private ICloudSession session;
+
+    /**
+     *
+     * The constructor.
+     *
+     * @param appleId Apple id (e-mail address) for authentication
+     * @param password Password used for authentication
+     * @param stateStorage Storage to save authentication state
+     */
+    public ICloudService(String appleId, String password, Storage<String> stateStorage) {
+        this.appleId = appleId;
+        this.password = password;
+        this.clientId = "auth-" + UUID.randomUUID().toString().toLowerCase();
+
+        this.session = new ICloudSession(stateStorage);
+        this.session.setDefaultHeaders(Pair.of("Origin", HOME_ENDPOINT), Pair.of("Referer", HOME_ENDPOINT + "/"));
+    }
+
+    /**
+     * Initiate authentication
+     *
+     * @param forceRefresh Force a new authentication
+     * @return {@code true} if authentication was successful
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if request was interrupted
+     */
+    public boolean authenticate(boolean forceRefresh) throws IOException, InterruptedException {
+        boolean loginSuccessful = false;
+        if (this.session.getSessionToken() != null && !forceRefresh) {
+            try {
+                this.data = validateToken();
+                logger.debug("Token is valid.");
+                loginSuccessful = true;
+            } catch (ICloudApiResponseException ex) {
+                logger.debug("Token is not valid. Attemping new login.", ex);
+            }
+        }
+
+        if (!loginSuccessful) {
+            logger.debug("Authenticating as {}...", this.appleId);
+
+            Map<String, Object> requestBody = new HashMap<>();
+            requestBody.put("accountName", this.appleId);
+            requestBody.put("password", this.password);
+            requestBody.put("rememberMe", true);
+            if (session.hasToken()) {
+                requestBody.put("trustTokens", new String[] { this.session.getTrustToken() });
+            } else {
+                requestBody.put("trustTokens", new String[0]);
+            }
+
+            List<Pair<String, String>> headers = getAuthHeaders();
+
+            try {
+                this.session.post(AUTH_ENDPOINT + "/signin?isRememberMeEnabled=true", JsonUtils.toJson(requestBody),
+                        headers);
+            } catch (ICloudApiResponseException ex) {
+                return false;
+            }
+        }
+        return authenticateWithToken();
+    }
+
+    /**
+     * Try authentication with stored session token. Returns {@code true} if authentication was successful.
+     *
+     * @return {@code true} if authentication was successful
+     *
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this request was interrupted
+     *
+     */
+    public boolean authenticateWithToken() throws IOException, InterruptedException {
+        Map<String, Object> requestBody = new HashMap<>();
+
+        String accountCountry = session.getAccountCountry();
+        if (accountCountry != null) {
+            requestBody.put("accountCountryCode", accountCountry);
+        }
+
+        String sessionToken = session.getSessionToken();
+        if (sessionToken != null) {
+            requestBody.put("dsWebAuthToken", sessionToken);
+        }
+
+        requestBody.put("extended_login", true);
+
+        if (session.hasToken()) {
+            String token = session.getTrustToken();
+            if (token != null) {
+                requestBody.put("trustToken", token);
+            }
+        } else {
+            requestBody.put("trustToken", "");
+        }
+
+        try {
+            @Nullable
+            Map<String, Object> localSessionData = JsonUtils
+                    .toMap(session.post(SETUP_ENDPOINT + "/accountLogin", JsonUtils.toJson(requestBody), null));
+            if (localSessionData != null) {
+                data = localSessionData;
+            }
+        } catch (ICloudApiResponseException ex) {
+            logger.debug("Invalid authentication.");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @param pair
+     * @return
+     */
+    private List<Pair<String, String>> getAuthHeaders() {
+        return new ArrayList<>(List.of(Pair.of("Accept", "*/*"), Pair.of("Content-Type", "application/json"),
+                Pair.of("X-Apple-OAuth-Client-Id", ICLOUD_CLIENT_ID),
+                Pair.of("X-Apple-OAuth-Client-Type", "firstPartyAuth"),
+                Pair.of("X-Apple-OAuth-Redirect-URI", HOME_ENDPOINT),
+                Pair.of("X-Apple-OAuth-Require-Grant-Code", "true"),
+                Pair.of("X-Apple-OAuth-Response-Mode", "web_message"), Pair.of("X-Apple-OAuth-Response-Type", "code"),
+                Pair.of("X-Apple-OAuth-State", this.clientId), Pair.of("X-Apple-Widget-Key", ICLOUD_CLIENT_ID)));
+    }
+
+    private Map<String, Object> validateToken() throws IOException, InterruptedException, ICloudApiResponseException {
+        logger.debug("Checking session token validity");
+        String result = session.post(SETUP_ENDPOINT + "/validate", null, null);
+        logger.debug("Session token is still valid");
+
+        @Nullable
+        Map<String, Object> localSessionData = JsonUtils.toMap(result);
+        if (localSessionData == null) {
+            throw new IOException("Unable to create data object from json response");
+        }
+        return localSessionData;
+    }
+
+    /**
+     * Checks if 2-FA authentication is required.
+     *
+     * @return {@code true} if 2-FA authentication ({@link #validate2faCode(String)}) is required.
+     */
+    public boolean requires2fa() {
+        if (this.data.containsKey("dsInfo")) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> dsInfo = (@Nullable Map<String, Object>) this.data.get("dsInfo");
+            if (dsInfo != null && ((Double) dsInfo.getOrDefault("hsaVersion", "0")) == 2.0) {
+                return (this.data.containsKey("hsaChallengeRequired")
+                        && ((Boolean) this.data.getOrDefault("hsaChallengeRequired", Boolean.FALSE)
+                                || !isTrustedSession()));
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if session is trusted.
+     *
+     * @return {@code true} if session is trusted. Call {@link #trustSession()} if not.
+     */
+    public boolean isTrustedSession() {
+        return (Boolean) this.data.getOrDefault("hsaTrustedBrowser", Boolean.FALSE);
+    }
+
+    /**
+     * Provides 2-FA code to establish trusted session.
+     *
+     * @param code Code given by user for 2-FA authentication.
+     * @return {@code true} if code was accepted
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     */
+    public boolean validate2faCode(String code) throws IOException, InterruptedException, ICloudApiResponseException {
+        Map<String, Object> requestBody = Map.of("securityCode", Map.of("code", code));
+
+        List<Pair<String, String>> headers = ListUtil.replaceEntries(getAuthHeaders(),
+                List.of(Pair.of("Accept", "application/json")));
+
+        addSessionHeaders(headers);
+
+        try {
+            this.session.post(AUTH_ENDPOINT + "/verify/trusteddevice/securitycode", JsonUtils.toJson(requestBody),
+                    headers);
+        } catch (ICloudApiResponseException ex) {
+            logger.debug("Code verification failed.", ex);
+            return false;
+        }
+
+        logger.debug("Code verification successful.");
+
+        trustSession();
+        return true;
+    }
+
+    private void addSessionHeaders(List<Pair<String, String>> headers) {
+        String scnt = session.getScnt();
+        if (scnt != null && !scnt.isEmpty()) {
+            headers.add(Pair.of("scnt", scnt));
+        }
+
+        String sessionId = session.getSessionId();
+        if (sessionId != null && !sessionId.isEmpty()) {
+            headers.add(Pair.of("X-Apple-ID-Session-Id", sessionId));
+        }
+    }
+
+    private @Nullable String getWebserviceUrl(String wsKey) {
+        try {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> webservices = (@Nullable Map<String, Object>) data.get("webservices");
+            if (webservices == null) {
+                return null;
+            }
+            if (webservices.get(wsKey) instanceof Map) {
+                @SuppressWarnings("unchecked")
+                Map<String, ?> wsMap = (@Nullable Map<String, ?>) webservices.get(wsKey);
+                if (wsMap == null) {
+                    logger.error("Webservices result map has not expected format.");
+                    return null;
+                }
+                return (String) wsMap.get("url");
+            } else {
+                logger.error("Webservices result map has not expected format.");
+                return null;
+            }
+        } catch (ClassCastException e) {
+            logger.error("ClassCastException, map has not expected format.", e);
+            return null;
+        }
+    }
+
+    /**
+     * Establish trust for current session.
+     *
+     * @return {@code true} if successful.
+     *
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     *
+     */
+    public boolean trustSession() throws IOException, InterruptedException, ICloudApiResponseException {
+        List<Pair<String, String>> headers = getAuthHeaders();
+
+        addSessionHeaders(headers);
+        this.session.get(AUTH_ENDPOINT + "/2sv/trust", headers);
+        return authenticateWithToken();
+    }
+
+    /**
+     * Get access to find my iPhone service.
+     *
+     * @return Instance of {@link FindMyIPhoneServiceManager} for this session.
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this request was interrupted
+     */
+    public FindMyIPhoneServiceManager getDevices() throws IOException, InterruptedException {
+        String webserviceUrl = getWebserviceUrl("findme");
+        if (webserviceUrl != null) {
+            return new FindMyIPhoneServiceManager(this.session, webserviceUrl);
+        } else {
+            throw new IllegalStateException("Webservice URLs not set. Need to authenticate first.");
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudSession.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/ICloudSession.java
new file mode 100644 (file)
index 0000000..475b4a1
--- /dev/null
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal;
+
+import java.io.IOException;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpClient.Redirect;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.icloud.internal.utilities.CustomCookieStore;
+import org.openhab.binding.icloud.internal.utilities.JsonUtils;
+import org.openhab.binding.icloud.internal.utilities.ListUtil;
+import org.openhab.binding.icloud.internal.utilities.Pair;
+import org.openhab.core.storage.Storage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ * Class to handle iCloud API session information for accessing the API.
+ *
+ * The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+@NonNullByDefault
+public class ICloudSession {
+
+    private final Logger logger = LoggerFactory.getLogger(ICloudSession.class);
+
+    private final HttpClient client;
+
+    private List<Pair<String, String>> headers = new ArrayList<>();
+
+    private ICloudSessionData data = new ICloudSessionData();
+
+    private Storage<String> stateStorage;
+
+    private static final String SESSION_DATA_KEY = "SESSION_DATA";
+
+    /**
+     * The constructor.
+     *
+     * @param stateStorage Storage to persist session state.
+     */
+    public ICloudSession(Storage<String> stateStorage) {
+        String storedData = stateStorage.get(SESSION_DATA_KEY);
+        if (storedData != null) {
+            ICloudSessionData localSessionData = JsonUtils.fromJson(storedData, ICloudSessionData.class);
+            if (localSessionData != null) {
+                data = localSessionData;
+            }
+        }
+        this.stateStorage = stateStorage;
+        client = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL)
+                .connectTimeout(Duration.ofSeconds(20))
+                .cookieHandler(new CookieManager(new CustomCookieStore(), CookiePolicy.ACCEPT_ALL)).build();
+    }
+
+    /**
+     * Invoke an HTTP POST request to the given url and body.
+     *
+     * @param url URL to call.
+     * @param body Body for the request
+     * @param overrideHeaders Â If not null the given headers are used instead of the standard headers set via
+     *            {@link #setDefaultHeaders(Pair...)} (optional)
+     * @return Result body as {@link String}.
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this blocking request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     */
+    public String post(String url, @Nullable String body, @Nullable List<Pair<String, String>> overrideHeaders)
+            throws IOException, InterruptedException, ICloudApiResponseException {
+        return request("POST", url, body, overrideHeaders);
+    }
+
+    /**
+     * Invoke an HTTP GET request to the given url.
+     *
+     * @param url URL to call.
+     * @param overrideHeaders Â If not null the given headers are used to replace corresponding entries of the standard
+     *            headers set via
+     *            {@link #setDefaultHeaders(Pair...)}
+     * @return Result body as {@link String}.
+     * @throws IOException if I/O error occurred
+     * @throws InterruptedException if this blocking request was interrupted
+     * @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
+     */
+    public String get(String url, List<Pair<String, String>> overrideHeaders)
+            throws IOException, InterruptedException, ICloudApiResponseException {
+        return request("GET", url, null, overrideHeaders);
+    }
+
+    private String request(String method, String url, @Nullable String body,
+            @Nullable List<Pair<String, String>> overrideHeaders)
+            throws IOException, InterruptedException, ICloudApiResponseException {
+        logger.debug("iCloud request {} {}.", method, url);
+
+        Builder builder = HttpRequest.newBuilder().uri(URI.create(url));
+
+        List<Pair<String, String>> requestHeaders = ListUtil.replaceEntries(this.headers, overrideHeaders);
+
+        for (Pair<String, String> header : requestHeaders) {
+            builder.header(header.getKey(), header.getValue());
+        }
+
+        if (body != null) {
+            builder.method(method, BodyPublishers.ofString(body));
+        }
+
+        HttpRequest request = builder.build();
+
+        logger.trace("Calling {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, request.headers(), body);
+
+        HttpResponse<?> response = this.client.send(request, BodyHandlers.ofString());
+
+        Object responseBody = response.body();
+        String responseBodyAsString = responseBody != null ? responseBody.toString() : "";
+
+        logger.trace("Result {} {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, response.statusCode(),
+                response.headers(), responseBodyAsString);
+
+        if (response.statusCode() >= 300) {
+            throw new ICloudApiResponseException(url, response.statusCode());
+        }
+
+        // Store headers to reuse authentication
+        this.data.accountCountry = response.headers().firstValue("X-Apple-ID-Account-Country")
+                .orElse(getAccountCountry());
+        this.data.sessionId = response.headers().firstValue("X-Apple-ID-Session-Id").orElse(getSessionId());
+        this.data.sessionToken = response.headers().firstValue("X-Apple-Session-Token").orElse(getSessionToken());
+        this.data.trustToken = response.headers().firstValue("X-Apple-TwoSV-Trust-Token").orElse(getTrustToken());
+        this.data.scnt = response.headers().firstValue("scnt").orElse(getScnt());
+
+        this.stateStorage.put(SESSION_DATA_KEY, JsonUtils.toJson(this.data));
+
+        return responseBodyAsString;
+    }
+
+    /**
+     * Sets default HTTP headers, for HTTP requests.
+     *
+     * @param headers HTTP headers to use for requests
+     */
+    @SafeVarargs
+    public final void setDefaultHeaders(Pair<String, String>... headers) {
+        this.headers = Arrays.asList(headers);
+    }
+
+    /**
+     * @return scnt
+     */
+    public @Nullable String getScnt() {
+        return data.scnt;
+    }
+
+    /**
+     * @return sessionId
+     */
+    public @Nullable String getSessionId() {
+        return data.sessionId;
+    }
+
+    /**
+     * @return sessionToken
+     */
+    public @Nullable String getSessionToken() {
+        return data.sessionToken;
+    }
+
+    /**
+     * @return trustToken
+     */
+    public @Nullable String getTrustToken() {
+        return data.trustToken;
+    }
+
+    /**
+     * @return {@code true} if session token is not empty.
+     */
+    public boolean hasToken() {
+        String sessionToken = data.sessionToken;
+        return sessionToken != null && !sessionToken.isEmpty();
+    }
+
+    /**
+     * @return accountCountry
+     */
+    public @Nullable String getAccountCountry() {
+        return data.accountCountry;
+    }
+
+    /**
+     *
+     * Internal class to encapsulate data required for iCloud authentication.
+     *
+     * @author Simon Spielmann Initial Contribution
+     */
+    private class ICloudSessionData {
+        @Nullable
+        String scnt;
+
+        @Nullable
+        String sessionId;
+
+        @Nullable
+        String sessionToken;
+
+        @Nullable
+        String trustToken;
+
+        @Nullable
+        String accountCountry;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/RetryException.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/RetryException.java
new file mode 100644 (file)
index 0000000..26a1a17
--- /dev/null
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This exception is thrown when a retry finally fails.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+@NonNullByDefault
+public class RetryException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * The constructor.
+     *
+     * @param originalException Exception which was thrown for the last unsuccessful retry.
+     */
+    public RetryException(@Nullable Throwable originalException) {
+        super("Retry finally failed.", originalException);
+    }
+}
index ebf095230641af765d307fe219a3f95ceb94ba8e..429bfab6a25d8d941f81f214df99145e1a4da7f4 100644 (file)
@@ -26,4 +26,5 @@ public class ICloudAccountThingConfiguration {
     public @Nullable String appleId;
     public @Nullable String password;
     public int refreshTimeInMinutes = 10;
+    public @Nullable String code;
 }
index baa52cb7efa752b4bfcdbc24da0b3c97645ae63a..75fb58fc512e224ae8bf28a7bbea00ab2e4d4ac8 100644 (file)
@@ -19,7 +19,7 @@ import java.util.List;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
 import org.openhab.binding.icloud.internal.handler.ICloudAccountBridgeHandler;
-import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
 import org.openhab.binding.icloud.internal.utilities.ICloudTextTranslator;
 import org.openhab.core.config.discovery.AbstractDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryResult;
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/AuthState.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/AuthState.java
new file mode 100644 (file)
index 0000000..d836000
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Enum to mark state during iCloud authentication.
+ *
+ * @author Simon Spielmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+public enum AuthState {
+
+    /**
+     * Authentication was not tried yet.
+     */
+    INITIAL,
+
+    /**
+     * Entered credentials (apple id / password) are invalid.
+     */
+    USER_PW_INVALID,
+
+    /**
+     * Waiting for user to provide 2-FA code in thing configuration.
+     */
+    WAIT_FOR_CODE,
+
+    /**
+     * Sucessfully authenticated.
+     */
+    AUTHENTICATED
+
+}
index 922dfd7f25dc1bd12a03030a8b58f0b4d596b704..8131f0e75ae5806fb59f0321728b07c1587e24b9 100644 (file)
@@ -15,21 +15,26 @@ package org.openhab.binding.icloud.internal.handler;
 import static java.util.concurrent.TimeUnit.*;
 
 import java.io.IOException;
-import java.net.URISyntaxException;
-import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.binding.icloud.internal.ICloudConnection;
+import org.openhab.binding.icloud.internal.ICloudApiResponseException;
 import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
-import org.openhab.binding.icloud.internal.ICloudDeviceInformationParser;
+import org.openhab.binding.icloud.internal.ICloudService;
+import org.openhab.binding.icloud.internal.RetryException;
 import org.openhab.binding.icloud.internal.configuration.ICloudAccountThingConfiguration;
-import org.openhab.binding.icloud.internal.json.response.ICloudAccountDataResponse;
-import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
+import org.openhab.binding.icloud.internal.utilities.JsonUtils;
 import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.storage.Storage;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.ThingStatus;
@@ -37,19 +42,18 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.binding.BaseBridgeHandler;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
-import org.osgi.framework.ServiceRegistration;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.gson.JsonSyntaxException;
 
 /**
- * Retrieves the data for a given account from iCloud and passes the
- * information to {@link org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery} and to the
- * {@link ICloudDeviceHandler}s.
+ * Retrieves the data for a given account from iCloud and passes the information to
+ * {@link org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery} and to the {@link ICloudDeviceHandler}s.
  *
  * @author Patrik Gfeller - Initial contribution
  * @author Hans-Jörg Merk - Extended support with initial Contribution
+ * @author Simon Spielmann - Rework for new iCloud API
  */
 @NonNullByDefault
 public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
@@ -58,23 +62,36 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
 
     private static final int CACHE_EXPIRY = (int) SECONDS.toMillis(10);
 
-    private final ICloudDeviceInformationParser deviceInformationParser = new ICloudDeviceInformationParser();
-    private @Nullable ICloudConnection connection;
+    private @Nullable ICloudService iCloudService;
+
     private @Nullable ExpiringCache<String> iCloudDeviceInformationCache;
 
-    @Nullable
-    ServiceRegistration<?> service;
+    private AuthState authState = AuthState.INITIAL;
 
     private final Object synchronizeRefresh = new Object();
 
-    private List<ICloudDeviceInformationListener> deviceInformationListeners = Collections
-            .synchronizedList(new ArrayList<>());
+    private Set<ICloudDeviceInformationListener> deviceInformationListeners = Collections
+            .synchronizedSet(new HashSet<>());
 
     @Nullable
     ScheduledFuture<?> refreshJob;
 
-    public ICloudAccountBridgeHandler(Bridge bridge) {
+    @Nullable
+    ScheduledFuture<?> initTask;
+
+    private Storage<String> storage;
+
+    private static final String AUTH_CODE_KEY = "AUTH_CODE";
+
+    /**
+     * The constructor.
+     *
+     * @param bridge The bridge to set
+     * @param storage The storage service to set.
+     */
+    public ICloudAccountBridgeHandler(Bridge bridge, Storage<String> storage) {
         super(bridge);
+        this.storage = storage;
     }
 
     @Override
@@ -86,21 +103,161 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
         }
     }
 
+    @SuppressWarnings("null")
     @Override
     public void initialize() {
         logger.debug("iCloud bridge handler initializing ...");
-        iCloudDeviceInformationCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
-            try {
-                return connection.requestDeviceStatusJSON();
-            } catch (IOException e) {
-                logger.warn("Unable to refresh device data", e);
-                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+
+        if (authState != AuthState.WAIT_FOR_CODE) {
+            authState = AuthState.INITIAL;
+        }
+
+        this.iCloudDeviceInformationCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
+            return callApiWithRetryAndExceptionHandling(() -> {
+                // callApiWithRetryAndExceptionHanlding ensures that iCloudService is not null when the following is
+                // called. Cannot use method local iCloudService instance here, because instance may be replaced with a
+                // new
+                // one during retry.
+                return iCloudService.getDevices().refreshClient();
+            });
+
+        });
+
+        updateStatus(ThingStatus.UNKNOWN);
+
+        // Init has to be done async becaue it requires sync network calls, which are not allowed in init.
+        Callable<?> asyncInit = () -> {
+            callApiWithRetryAndExceptionHandling(() -> {
+                logger.debug("Dummy call for initial authentication.");
+                return null;
+            });
+            if (authState == AuthState.AUTHENTICATED) {
+                ICloudAccountThingConfiguration config = getConfigAs(ICloudAccountThingConfiguration.class);
+                this.refreshJob = this.scheduler.scheduleWithFixedDelay(this::refreshData, 0,
+                        config.refreshTimeInMinutes, MINUTES);
+            } else {
+                cancelRefresh();
+            }
+            return null;
+        };
+        initTask = this.scheduler.schedule(asyncInit, 0, TimeUnit.SECONDS);
+        logger.debug("iCloud bridge handler initialized.");
+    }
+
+    private <@Nullable T> T callApiWithRetryAndExceptionHandling(Callable<T> wrapped) {
+        int retryCount = 1;
+        boolean success = false;
+        Throwable lastException = null;
+        synchronized (synchronizeRefresh) {
+            if (this.iCloudService == null) {
+                ICloudAccountThingConfiguration config = getConfigAs(ICloudAccountThingConfiguration.class);
+                final String localAppleId = config.appleId;
+                final String localPassword = config.password;
+
+                if (localAppleId != null && localPassword != null) {
+                    this.iCloudService = new ICloudService(localAppleId, localPassword, this.storage);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                            "Apple ID or password is not set!");
+                    return null;
+                }
+            }
+
+            if (authState == AuthState.INITIAL) {
+                success = checkLogin();
+            } else if (authState == AuthState.WAIT_FOR_CODE) {
+                try {
+                    success = handle2FAAuthentication();
+                } catch (IOException | InterruptedException | ICloudApiResponseException ex) {
+                    logger.debug("Error while validating 2-FA code.", ex);
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                            "Error while validating 2-FA code.");
+                    return null;
+                }
+            }
+            if (authState != AuthState.AUTHENTICATED && !success) {
                 return null;
             }
-        });
 
-        startHandler();
-        logger.debug("iCloud bridge initialized.");
+            do {
+                try {
+                    if (authState == AuthState.AUTHENTICATED) {
+                        return wrapped.call();
+                    } else {
+                        checkLogin();
+                    }
+                } catch (ICloudApiResponseException e) {
+                    logger.debug("ICloudApiResponseException with status code {}", e.getStatusCode());
+                    lastException = e;
+                    if (e.getStatusCode() == 450) {
+                        checkLogin();
+                    }
+                } catch (IllegalStateException e) {
+                    logger.debug("Need to authenticate first.", e);
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Wait for login");
+                    return null;
+                } catch (IOException e) {
+                    logger.warn("Unable to refresh device data", e);
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+                    return null;
+                } catch (Exception e) {
+                    logger.debug("Unexpected exception occured", e);
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+                    return null;
+                }
+
+                retryCount++;
+                try {
+                    Thread.sleep(200);
+                } catch (InterruptedException e) {
+                    Thread.interrupted();
+                }
+            } while (!success && retryCount < 3);
+            throw new RetryException(lastException);
+        }
+    }
+
+    private boolean handle2FAAuthentication() throws IOException, InterruptedException, ICloudApiResponseException {
+        logger.debug("Starting iCloud 2-FA authentication  AuthState={}, Thing={})...", authState,
+                getThing().getUID().getAsString());
+        final ICloudService localICloudService = this.iCloudService;
+        if (authState != AuthState.WAIT_FOR_CODE || localICloudService == null) {
+            throw new IllegalStateException("2-FA authentication not initialized.");
+        }
+        ICloudAccountThingConfiguration config = getConfigAs(ICloudAccountThingConfiguration.class);
+        String lastTriedCode = storage.get(AUTH_CODE_KEY);
+        String code = config.code;
+        boolean success = false;
+        if (code == null || code.isBlank() || code.equals(lastTriedCode)) {
+            // Still waiting for user to update config.
+            logger.warn("ICloud authentication requires 2-FA code. Please provide code configuration for thing '{}'.",
+                    getThing().getUID().getAsString());
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+                    "Please provide 2-FA code in thing configuration.");
+            return false;
+        } else {
+            // 2-FA-Code was requested in previous call of this method.
+            // User has provided code in config.
+            logger.debug("Code is given in thing configuration '{}'. Trying to validate code...",
+                    getThing().getUID().getAsString());
+            storage.put(AUTH_CODE_KEY, lastTriedCode);
+            success = localICloudService.validate2faCode(code);
+            if (!success) {
+                authState = AuthState.INITIAL;
+                logger.warn("ICloud token invalid.");
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid 2-FA-code.");
+                return false;
+            }
+            org.openhab.core.config.core.Configuration config2 = editConfiguration();
+            config2.put("code", "");
+            updateConfiguration(config2);
+
+            logger.debug("Code is valid.");
+        }
+        authState = AuthState.AUTHENTICATED;
+        updateStatus(ThingStatus.ONLINE);
+        logger.debug("iCloud bridge handler '{}' authenticated with 2-FA code.", getThing().getUID().getAsString());
+        return success;
     }
 
     @Override
@@ -110,55 +267,126 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
 
     @Override
     public void dispose() {
-        if (refreshJob != null) {
-            refreshJob.cancel(true);
+        cancelRefresh();
+
+        final ScheduledFuture<?> localInitTask = this.initTask;
+        if (localInitTask != null) {
+            localInitTask.cancel(true);
+            this.initTask = null;
         }
         super.dispose();
     }
 
-    public void findMyDevice(String deviceId) throws IOException {
-        if (connection == null) {
-            logger.debug("Can't send Find My Device request, because connection is null!");
-            return;
+    private void cancelRefresh() {
+        final ScheduledFuture<?> localrefreshJob = this.refreshJob;
+        if (localrefreshJob != null) {
+            localrefreshJob.cancel(true);
+            this.refreshJob = null;
         }
-        connection.findMyDevice(deviceId);
+    }
+
+    @SuppressWarnings("null")
+    public void findMyDevice(String deviceId) throws IOException, InterruptedException {
+        callApiWithRetryAndExceptionHandling(() -> {
+            // callApiWithRetryAndExceptionHanlding ensures that iCloudService is not null when the following is
+            // called. Cannot use method local iCloudService instance here, because instance may be replaced with a new
+            // one during retry.
+            iCloudService.getDevices().playSound(deviceId);
+            return null;
+        });
     }
 
     public void registerListener(ICloudDeviceInformationListener listener) {
-        deviceInformationListeners.add(listener);
+        this.deviceInformationListeners.add(listener);
     }
 
     public void unregisterListener(ICloudDeviceInformationListener listener) {
-        deviceInformationListeners.remove(listener);
+        this.deviceInformationListeners.remove(listener);
     }
 
-    private void startHandler() {
+    /**
+     * Checks login to iCloud account. The flow is a bit complicated due to 2-FA authentication.
+     * The normal flow would be:
+     *
+     *
+     * <pre>
+        ICloudService service = new ICloudService(...);
+        service.authenticate(false);
+        if (service.requires2fa()) {
+            String code = ... // Request code from user!
+            System.out.println(service.validate2faCode(code));
+            if (!service.isTrustedSession()) {
+                service.trustSession();
+            }
+            if (!service.isTrustedSession()) {
+                System.err.println("Trust failed!!!");
+            }
+     * </pre>
+     *
+     * The call to {@link ICloudService#authenticate(boolean)} request a token from the user.
+     * This should be done only once. Afterwards the user has to update the configuration.
+     * In openhab this method here is called for several reason (e.g. config change). So we track if we already
+     * requested a code {@link #validate2faCode}.
+     */
+    private boolean checkLogin() {
+        logger.debug("Starting iCloud authentication (AuthState={}, Thing={})...", authState,
+                getThing().getUID().getAsString());
+        final ICloudService localICloudService = this.iCloudService;
+        if (authState == AuthState.WAIT_FOR_CODE || localICloudService == null) {
+            throw new IllegalStateException("2-FA authentication not completed.");
+        }
+
         try {
-            logger.debug("iCloud bridge starting handler ...");
-            ICloudAccountThingConfiguration config = getConfigAs(ICloudAccountThingConfiguration.class);
-            final String localAppleId = config.appleId;
-            final String localPassword = config.password;
-            if (localAppleId != null && localPassword != null) {
-                connection = new ICloudConnection(localAppleId, localPassword);
-            } else {
+            // No code requested yet or session is trusted (hopefully).
+            boolean success = localICloudService.authenticate(false);
+            if (!success) {
+                authState = AuthState.USER_PW_INVALID;
+                logger.warn("iCloud authentication failed. Invalid credentials.");
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid credentials.");
+                this.iCloudService = null;
+                return false;
+            }
+            if (localICloudService.requires2fa()) {
+                // New code was requested. Wait for the user to update config.
+                logger.warn(
+                        "iCloud authentication requires 2-FA code. Please provide code configuration for thing '{}'.",
+                        getThing().getUID().getAsString());
+                authState = AuthState.WAIT_FOR_CODE;
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
-                        "Apple ID/Password is not set!");
-                return;
+                        "Please provide 2-FA code in thing configuration.");
+                return false;
             }
-            refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, config.refreshTimeInMinutes, MINUTES);
 
-            logger.debug("iCloud bridge handler started.");
-        } catch (URISyntaxException e) {
-            logger.debug("Something went wrong while constructing the connection object", e);
+            if (!localICloudService.isTrustedSession()) {
+                logger.debug("Trying to establish session trust.");
+                success = localICloudService.trustSession();
+                if (!success) {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Session trust failed.");
+                    return false;
+                }
+            }
+
+            authState = AuthState.AUTHENTICATED;
+            updateStatus(ThingStatus.ONLINE);
+            logger.debug("iCloud bridge handler authenticated.");
+            return true;
+        } catch (Exception e) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+            return false;
         }
     }
 
+    /**
+     * Refresh iCloud device data.
+     */
     public void refreshData() {
-        synchronized (synchronizeRefresh) {
-            logger.debug("iCloud bridge refreshing data ...");
-
-            String json = iCloudDeviceInformationCache.getValue();
+        logger.debug("iCloud bridge refreshing data ...");
+        synchronized (this.synchronizeRefresh) {
+            ExpiringCache<String> localCache = this.iCloudDeviceInformationCache;
+            if (localCache == null) {
+                return;
+            }
+            String json = localCache.getValue();
             logger.trace("json: {}", json);
 
             if (json == null) {
@@ -166,7 +394,7 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
             }
 
             try {
-                ICloudAccountDataResponse iCloudData = deviceInformationParser.parse(json);
+                ICloudAccountDataResponse iCloudData = JsonUtils.fromJson(json, ICloudAccountDataResponse.class);
                 if (iCloudData == null) {
                     return;
                 }
index 49ab4f0fec60f31a3af38397bd502976718febb2..9996320afce2bc37c37e51495397548bf5b26664 100644 (file)
@@ -27,7 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
 import org.openhab.binding.icloud.internal.configuration.ICloudDeviceThingConfiguration;
-import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
 import org.openhab.core.library.types.DateTimeType;
 import org.openhab.core.library.types.DecimalType;
 import org.openhab.core.library.types.OnOffType;
@@ -36,6 +36,8 @@ import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ChannelUID;
 import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.binding.BaseThingHandler;
 import org.openhab.core.thing.binding.ThingHandler;
 import org.openhab.core.types.Command;
@@ -51,13 +53,14 @@ import org.slf4j.LoggerFactory;
  * @author Patrik Gfeller - Initial contribution
  * @author Hans-Jörg Merk - Helped with testing and feedback
  * @author Gaël L'hopital - Added low battery
+ * @author Simon Spielmann - Rework for new iCloud API
  *
  */
 @NonNullByDefault
 public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDeviceInformationListener {
     private final Logger logger = LoggerFactory.getLogger(ICloudDeviceHandler.class);
+
     private @Nullable String deviceId;
-    private @Nullable ICloudAccountBridgeHandler icloudAccount;
 
     public ICloudDeviceHandler(Thing thing) {
         super(thing);
@@ -67,7 +70,7 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
     public void deviceInformationUpdate(List<ICloudDeviceInformation> deviceInformationList) {
         ICloudDeviceInformation deviceInformationRecord = getDeviceInformationRecord(deviceInformationList);
         if (deviceInformationRecord != null) {
-            if (deviceInformationRecord.getDeviceStatus() == 200 || deviceInformationRecord.getDeviceStatus() == 203) {
+            if (deviceInformationRecord.getDeviceStatus() == 200) {
                 updateStatus(ONLINE);
             } else {
                 updateStatus(OFFLINE, COMMUNICATION_ERROR, "Reported offline by iCloud webservice");
@@ -91,35 +94,42 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
 
     @Override
     public void initialize() {
-        Bridge bridge = getBridge();
-        Object bridgeStatus = (bridge == null) ? null : bridge.getStatus();
-        logger.debug("initializeThing thing [{}]; bridge status: [{}]", getThing().getUID(), bridgeStatus);
-
         ICloudDeviceThingConfiguration configuration = getConfigAs(ICloudDeviceThingConfiguration.class);
         this.deviceId = configuration.deviceId;
 
-        ICloudAccountBridgeHandler handler = getIcloudAccount();
-        if (handler != null) {
-            refreshData();
-        } else {
-            updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "Bridge not found");
-        }
-    }
-
-    private void refreshData() {
-        ICloudAccountBridgeHandler bridge = getIcloudAccount();
+        Bridge bridge = getBridge();
         if (bridge != null) {
-            bridge.refreshData();
+            ICloudAccountBridgeHandler handler = (ICloudAccountBridgeHandler) bridge.getHandler();
+            if (handler != null) {
+                handler.registerListener(this);
+                if (bridge.getStatus() == ThingStatus.ONLINE) {
+                    handler.refreshData();
+                    updateStatus(ThingStatus.ONLINE);
+                } else {
+                    updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
+                }
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
+                        "Bridge handler is not configured");
+            }
+        } else {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge is not configured");
         }
     }
 
     @Override
     public void handleCommand(ChannelUID channelUID, Command command) {
-        logger.trace("Command '{}' received for channel '{}'", command, channelUID);
+        this.logger.trace("Command '{}' received for channel '{}'", command, channelUID);
 
-        ICloudAccountBridgeHandler bridge = getIcloudAccount();
+        Bridge bridge = getBridge();
         if (bridge == null) {
-            logger.debug("No bridge found, ignoring command");
+            this.logger.debug("No bridge found, ignoring command");
+            return;
+        }
+
+        ICloudAccountBridgeHandler bridgeHandler = (ICloudAccountBridgeHandler) bridge.getHandler();
+        if (bridgeHandler == null) {
+            this.logger.debug("No bridge handler found, ignoring command");
             return;
         }
 
@@ -129,27 +139,30 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
                 try {
                     final String deviceId = this.deviceId;
                     if (deviceId == null) {
-                        logger.debug("Can't send Find My Device request, because deviceId is null!");
+                        this.logger.debug("Can't send Find My Device request, because deviceId is null!");
                         return;
                     }
-                    bridge.findMyDevice(deviceId);
-                } catch (IOException e) {
-                    logger.warn("Unable to execute find my device request", e);
+                    bridgeHandler.findMyDevice(deviceId);
+                } catch (IOException | InterruptedException e) {
+                    this.logger.warn("Unable to execute find my device request", e);
                 }
                 updateState(FIND_MY_PHONE, OnOffType.OFF);
             }
         }
 
         if (command instanceof RefreshType) {
-            bridge.refreshData();
+            bridgeHandler.refreshData();
         }
     }
 
     @Override
     public void dispose() {
-        ICloudAccountBridgeHandler bridge = getIcloudAccount();
+        Bridge bridge = getBridge();
         if (bridge != null) {
-            bridge.unregisterListener(this);
+            ThingHandler bridgeHandler = bridge.getHandler();
+            if (bridgeHandler instanceof ICloudAccountBridgeHandler) {
+                ((ICloudAccountBridgeHandler) bridgeHandler).unregisterListener(this);
+            }
         }
         super.dispose();
     }
@@ -169,7 +182,7 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
 
     private @Nullable ICloudDeviceInformation getDeviceInformationRecord(
             List<ICloudDeviceInformation> deviceInformationList) {
-        logger.debug("Device: [{}]", deviceId);
+        this.logger.debug("Device: [{}]", this.deviceId);
 
         for (ICloudDeviceInformation deviceInformationRecord : deviceInformationList) {
             String currentId = deviceInformationRecord.getId();
@@ -196,21 +209,4 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
 
         return dateTime;
     }
-
-    private @Nullable ICloudAccountBridgeHandler getIcloudAccount() {
-        if (icloudAccount == null) {
-            Bridge bridge = getBridge();
-            if (bridge == null) {
-                return null;
-            }
-            ThingHandler handler = bridge.getHandler();
-            if (handler instanceof ICloudAccountBridgeHandler) {
-                icloudAccount = (ICloudAccountBridgeHandler) handler;
-                icloudAccount.registerListener(this);
-            } else {
-                return null;
-            }
-        }
-        return icloudAccount;
-    }
 }
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountDataResponse.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountDataResponse.java
new file mode 100644 (file)
index 0000000..bdf1958
--- /dev/null
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+import java.util.List;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Serializable class to parse the device information json response
+ * received from the Apple server.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ */
+public class ICloudAccountDataResponse {
+
+    @SerializedName("content")
+    private List<ICloudDeviceInformation> iCloudDeviceInformationList;
+
+    @SerializedName("serverContext")
+    private ICloudServerContext iCloudServerContext;
+
+    @SerializedName("statusCode")
+    private String iCloudAccountStatusCode;
+
+    @SerializedName("userInfo")
+    private ICloudAccountUserInfo iCloudAccountUserInfo;
+
+    public List<ICloudDeviceInformation> getICloudDeviceInformationList() {
+        return iCloudDeviceInformationList;
+    }
+
+    public String getICloudAccountStatusCode() {
+        return iCloudAccountStatusCode;
+    }
+
+    public ICloudAccountUserInfo getICloudAccountUserInfo() {
+        return iCloudAccountUserInfo;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountUserInfo.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudAccountUserInfo.java
new file mode 100644 (file)
index 0000000..d339604
--- /dev/null
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudAccountUserInfo {
+    private int accountFormatter;
+
+    private String firstName;
+
+    private boolean hasMembers;
+
+    private String lastName;
+
+    private Object membersInfo;
+
+    public int getAccountFormatter() {
+        return this.accountFormatter;
+    }
+
+    public String getFirstName() {
+        return this.firstName;
+    }
+
+    public boolean getHasMembers() {
+        return this.hasMembers;
+    }
+
+    public String getLastName() {
+        return this.lastName;
+    }
+
+    public Object getMembersInfo() {
+        return this.membersInfo;
+    }
+
+    public void setAccountFormatter(int accountFormatter) {
+        this.accountFormatter = accountFormatter;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public void setHasMembers(boolean hasMembers) {
+        this.hasMembers = hasMembers;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public void setMembersInfo(Object membersInfo) {
+        this.membersInfo = membersInfo;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceFeatures.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceFeatures.java
new file mode 100644 (file)
index 0000000..9478f5e
--- /dev/null
@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudDeviceFeatures {
+
+    @SerializedName("CLK")
+    private boolean clk;
+
+    @SerializedName("CLT")
+    private boolean clt;
+
+    @SerializedName("CWP")
+    private boolean cwp;
+
+    @SerializedName("KEY")
+    private boolean key;
+
+    @SerializedName("KPD")
+    private boolean kpd;
+
+    @SerializedName("LCK")
+    private boolean lck;
+
+    @SerializedName("LKL")
+    private boolean lkl;
+
+    @SerializedName("LKM")
+    private boolean lkm;
+
+    @SerializedName("LLC")
+    private boolean llc;
+
+    @SerializedName("LMG")
+    private boolean lmg;
+
+    @SerializedName("LOC")
+    private boolean loc;
+
+    @SerializedName("LST")
+    private boolean lst;
+
+    @SerializedName("MCS")
+    private boolean mcs;
+
+    @SerializedName("MSG")
+    private boolean msg;
+
+    @SerializedName("PIN")
+    private boolean pin;
+
+    @SerializedName("REM")
+    private boolean rem;
+
+    @SerializedName("SND")
+    private boolean snd;
+
+    @SerializedName("SVP")
+    private boolean svp;
+
+    @SerializedName("TEU")
+    private boolean teu;
+
+    @SerializedName("WIP")
+    private boolean wip;
+
+    @SerializedName("WMG")
+    private boolean wmg;
+
+    @SerializedName("XRM")
+    private boolean xrm;
+
+    @SerializedName("CLT")
+
+    public boolean getCLK() {
+        return this.clk;
+    }
+
+    public boolean getClt() {
+        return this.clt;
+    }
+
+    public boolean getCwp() {
+        return this.cwp;
+    }
+
+    public boolean getKey() {
+        return this.key;
+    }
+
+    public boolean getKpd() {
+        return this.kpd;
+    }
+
+    public boolean getLck() {
+        return this.lck;
+    }
+
+    public boolean getLkl() {
+        return this.lkl;
+    }
+
+    public boolean getLkm() {
+        return this.lkm;
+    }
+
+    public boolean getLlc() {
+        return this.llc;
+    }
+
+    public boolean getLmg() {
+        return this.lmg;
+    }
+
+    public boolean getLoc() {
+        return this.loc;
+    }
+
+    public boolean getLst() {
+        return this.lst;
+    }
+
+    public boolean getMcs() {
+        return this.mcs;
+    }
+
+    public boolean getMsg() {
+        return this.msg;
+    }
+
+    public boolean getPin() {
+        return this.pin;
+    }
+
+    public boolean getRem() {
+        return this.rem;
+    }
+
+    public boolean getSnd() {
+        return this.snd;
+    }
+
+    public boolean getSvp() {
+        return this.svp;
+    }
+
+    public boolean getTeu() {
+        return this.teu;
+    }
+
+    public boolean getWip() {
+        return this.wip;
+    }
+
+    public boolean getWmg() {
+        return this.wmg;
+    }
+
+    public boolean getXrm() {
+        return this.xrm;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java
new file mode 100644 (file)
index 0000000..b562408
--- /dev/null
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+import java.util.ArrayList;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ * Contains device specific status information.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudDeviceInformation {
+    private boolean activationLocked;
+
+    private ArrayList<Object> audioChannels;
+
+    private double batteryLevel;
+
+    private String batteryStatus;
+
+    private boolean canWipeAfterLock;
+
+    private boolean darkWake;
+
+    private String deviceClass;
+
+    private String deviceColor;
+
+    private String deviceDisplayName;
+
+    private String deviceModel;
+
+    private int deviceStatus;
+
+    private ICloudDeviceFeatures features;
+
+    private boolean fmlyShare;
+
+    private String id;
+
+    private boolean isLocating;
+
+    private boolean isMac;
+
+    private ICloudDeviceLocation location;
+
+    private boolean locationCapable;
+
+    private boolean locationEnabled;
+
+    private boolean locFoundEnabled;
+
+    private Object lockedTimestamp;
+
+    private Object lostDevice;
+
+    private boolean lostModeCapable;
+
+    private boolean lostModeEnabled;
+
+    private String lostTimestamp;
+
+    private boolean lowPowerMode;
+
+    private int maxMsgChar;
+
+    private Object mesg;
+
+    private String modelDisplayName;
+
+    private Object msg;
+
+    private String name;
+
+    private int passcodeLength;
+
+    private String prsId;
+
+    private String rawDeviceModel;
+
+    private Object remoteLock;
+
+    private Object remoteWipe;
+
+    private Object snd;
+
+    private boolean thisDevice;
+
+    private Object trackingInfo;
+
+    private Object wipedTimestamp;
+
+    private boolean wipeInProgress;
+
+    public boolean getActivationLocked() {
+        return this.activationLocked;
+    }
+
+    public ArrayList<Object> getAudioChannels() {
+        return this.audioChannels;
+    }
+
+    public double getBatteryLevel() {
+        return this.batteryLevel;
+    }
+
+    public String getBatteryStatus() {
+        return this.batteryStatus;
+    }
+
+    public boolean getCanWipeAfterLock() {
+        return this.canWipeAfterLock;
+    }
+
+    public boolean getDarkWake() {
+        return this.darkWake;
+    }
+
+    public String getDeviceClass() {
+        return this.deviceClass;
+    }
+
+    public String getDeviceColor() {
+        return this.deviceColor;
+    }
+
+    public String getDeviceDisplayName() {
+        return this.deviceDisplayName;
+    }
+
+    public String getDeviceModel() {
+        return this.deviceModel;
+    }
+
+    public int getDeviceStatus() {
+        return this.deviceStatus;
+    }
+
+    public ICloudDeviceFeatures getFeatures() {
+        return this.features;
+    }
+
+    public boolean getFmlyShare() {
+        return this.fmlyShare;
+    }
+
+    public String getId() {
+        return this.id;
+    }
+
+    public boolean getIsLocating() {
+        return this.isLocating;
+    }
+
+    public boolean getIsMac() {
+        return this.isMac;
+    }
+
+    public ICloudDeviceLocation getLocation() {
+        return this.location;
+    }
+
+    public boolean getLocationCapable() {
+        return this.locationCapable;
+    }
+
+    public boolean getLocationEnabled() {
+        return this.locationEnabled;
+    }
+
+    public boolean getLocFoundEnabled() {
+        return this.locFoundEnabled;
+    }
+
+    public Object getLockedTimestamp() {
+        return this.lockedTimestamp;
+    }
+
+    public Object getLostDevice() {
+        return this.lostDevice;
+    }
+
+    public boolean getLostModeCapable() {
+        return this.lostModeCapable;
+    }
+
+    public boolean getLostModeEnabled() {
+        return this.lostModeEnabled;
+    }
+
+    public String getLostTimestamp() {
+        return this.lostTimestamp;
+    }
+
+    public boolean getLowPowerMode() {
+        return this.lowPowerMode;
+    }
+
+    public int getMaxMsgChar() {
+        return this.maxMsgChar;
+    }
+
+    public Object getMesg() {
+        return this.mesg;
+    }
+
+    public String getModelDisplayName() {
+        return this.modelDisplayName;
+    }
+
+    public Object getMsg() {
+        return this.msg;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public int getPasscodeLength() {
+        return this.passcodeLength;
+    }
+
+    public String getPrsId() {
+        return this.prsId;
+    }
+
+    public String getRawDeviceModel() {
+        return this.rawDeviceModel;
+    }
+
+    public Object getRemoteLock() {
+        return this.remoteLock;
+    }
+
+    public Object getRemoteWipe() {
+        return this.remoteWipe;
+    }
+
+    public Object getSnd() {
+        return this.snd;
+    }
+
+    public boolean getThisDevice() {
+        return this.thisDevice;
+    }
+
+    public Object getTrackingInfo() {
+        return this.trackingInfo;
+    }
+
+    public Object getWipedTimestamp() {
+        return this.wipedTimestamp;
+    }
+
+    public boolean getWipeInProgress() {
+        return this.wipeInProgress;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceLocation.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceLocation.java
new file mode 100644 (file)
index 0000000..3f9e4db
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ * Contains device location information.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudDeviceLocation {
+    private double altitude;
+
+    private int floorLevel;
+
+    private double horizontalAccuracy;
+
+    private boolean isInaccurate;
+
+    private boolean isOld;
+
+    private double latitude;
+
+    private boolean locationFinished;
+
+    private String locationType;
+
+    private double longitude;
+
+    private String positionType;
+
+    private long timeStamp;
+
+    private double verticalAccuracy;
+
+    public double getAltitude() {
+        return this.altitude;
+    }
+
+    public int getFloorLevel() {
+        return this.floorLevel;
+    }
+
+    public double getHorizontalAccuracy() {
+        return this.horizontalAccuracy;
+    }
+
+    public boolean getIsInaccurate() {
+        return this.isInaccurate;
+    }
+
+    public boolean getIsOld() {
+        return this.isOld;
+    }
+
+    public double getLatitude() {
+        return this.latitude;
+    }
+
+    public boolean getLocationFinished() {
+        return this.locationFinished;
+    }
+
+    public String getLocationType() {
+        return this.locationType;
+    }
+
+    public double getLongitude() {
+        return this.longitude;
+    }
+
+    public String getPositionType() {
+        return this.positionType;
+    }
+
+    public long getTimeStamp() {
+        return this.timeStamp;
+    }
+
+    public double getVerticalAccuracy() {
+        return this.verticalAccuracy;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContext.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContext.java
new file mode 100644 (file)
index 0000000..8c31fac
--- /dev/null
@@ -0,0 +1,204 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudServerContext {
+    @Nullable
+    private String authToken;
+
+    private int callbackIntervalInMS;
+
+    private boolean classicUser;
+
+    private String clientId;
+
+    private boolean cloudUser;
+
+    private String deviceLoadStatus;
+
+    private boolean enable2FAErase;
+
+    private boolean enable2FAFamilyActions;
+
+    private boolean enable2FAFamilyRemove;
+
+    private boolean enableMapStats;
+
+    private String imageBaseUrl;
+
+    private String info;
+
+    private boolean isHSA;
+
+    private Object lastSessionExtensionTime;
+
+    private int macCount;
+
+    private int maxCallbackIntervalInMS;
+
+    private int maxDeviceLoadTime;
+
+    private int maxLocatingTime;
+
+    private int minCallbackIntervalInMS;
+
+    private int minTrackLocThresholdInMts;
+
+    private String preferredLanguage;
+
+    private long prefsUpdateTime;
+
+    private String prsId;
+
+    private long serverTimestamp;
+
+    private int sessionLifespan;
+
+    private boolean showSllNow;
+
+    private ICloudServerContextTimezone timezone;
+
+    private int trackInfoCacheDurationInSecs;
+
+    private boolean useAuthWidget;
+
+    private boolean validRegion;
+
+    public String getAuthToken() {
+        return this.authToken;
+    }
+
+    public int getCallbackIntervalInMS() {
+        return this.callbackIntervalInMS;
+    }
+
+    public boolean getClassicUser() {
+        return this.classicUser;
+    }
+
+    public String getClientId() {
+        return this.clientId;
+    }
+
+    public boolean getCloudUser() {
+        return this.cloudUser;
+    }
+
+    public String getDeviceLoadStatus() {
+        return this.deviceLoadStatus;
+    }
+
+    public boolean getEnable2FAErase() {
+        return this.enable2FAErase;
+    }
+
+    public boolean getEnable2FAFamilyActions() {
+        return this.enable2FAFamilyActions;
+    }
+
+    public boolean getEnable2FAFamilyRemove() {
+        return this.enable2FAFamilyRemove;
+    }
+
+    public boolean getEnableMapStats() {
+        return this.enableMapStats;
+    }
+
+    public String getImageBaseUrl() {
+        return this.imageBaseUrl;
+    }
+
+    public String getInfo() {
+        return this.info;
+    }
+
+    public boolean getIsHSA() {
+        return this.isHSA;
+    }
+
+    public Object getLastSessionExtensionTime() {
+        return this.lastSessionExtensionTime;
+    }
+
+    public int getMacCount() {
+        return this.macCount;
+    }
+
+    public int getMaxCallbackIntervalInMS() {
+        return this.maxCallbackIntervalInMS;
+    }
+
+    public int getMaxDeviceLoadTime() {
+        return this.maxDeviceLoadTime;
+    }
+
+    public int getMaxLocatingTime() {
+        return this.maxLocatingTime;
+    }
+
+    public int getMinCallbackIntervalInMS() {
+        return this.minCallbackIntervalInMS;
+    }
+
+    public int getMinTrackLocThresholdInMts() {
+        return this.minTrackLocThresholdInMts;
+    }
+
+    public String getPreferredLanguage() {
+        return this.preferredLanguage;
+    }
+
+    public long getPrefsUpdateTime() {
+        return this.prefsUpdateTime;
+    }
+
+    public String getPrsId() {
+        return this.prsId;
+    }
+
+    public long getServerTimestamp() {
+        return this.serverTimestamp;
+    }
+
+    public int getSessionLifespan() {
+        return this.sessionLifespan;
+    }
+
+    public boolean getShowSllNow() {
+        return this.showSllNow;
+    }
+
+    public ICloudServerContextTimezone getTimezone() {
+        return this.timezone;
+    }
+
+    public int getTrackInfoCacheDurationInSecs() {
+        return this.trackInfoCacheDurationInSecs;
+    }
+
+    public boolean getUseAuthWidget() {
+        return this.useAuthWidget;
+    }
+
+    public boolean getValidRegion() {
+        return this.validRegion;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContextTimezone.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudServerContextTimezone.java
new file mode 100644 (file)
index 0000000..02fab81
--- /dev/null
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.handler.dto.json.response;
+
+/**
+ * Serializable class to parse json response received from the Apple server.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ *
+ */
+public class ICloudServerContextTimezone {
+    private int currentOffset;
+
+    private int previousOffset;
+
+    private long previousTransition;
+
+    private String tzCurrentName;
+
+    private String tzName;
+
+    public int getCurrentOffset() {
+        return this.currentOffset;
+    }
+
+    public int getPreviousOffset() {
+        return this.previousOffset;
+    }
+
+    public long getPreviousTransition() {
+        return this.previousTransition;
+    }
+
+    public String getTzCurrentName() {
+        return this.tzCurrentName;
+    }
+
+    public String getTzName() {
+        return this.tzName;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudAccountDataRequest.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudAccountDataRequest.java
deleted file mode 100644 (file)
index 0637b76..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.request;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * Serializable request for icloud device data.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-@NonNullByDefault
-public class ICloudAccountDataRequest {
-    @SuppressWarnings("unused")
-    private ClientContext clientContext;
-
-    private ICloudAccountDataRequest() {
-        this.clientContext = ClientContext.defaultInstance();
-    }
-
-    public static ICloudAccountDataRequest defaultInstance() {
-        return new ICloudAccountDataRequest();
-    }
-
-    public static class ClientContext {
-        @SuppressWarnings("unused")
-        private String appName = "FindMyiPhone";
-        @SuppressWarnings("unused")
-        private boolean fmly = true;
-        @SuppressWarnings("unused")
-        private String appVersion = "5.0";
-        @SuppressWarnings("unused")
-        private String buildVersion = "376";
-        @SuppressWarnings("unused")
-        private int clientTimestamp = 0;
-        @SuppressWarnings("unused")
-        private String deviceUDID = "";
-        @SuppressWarnings("unused")
-        private int inactiveTime = 1;
-        @SuppressWarnings("unused")
-        private String osVersion = "14.0";
-        @SuppressWarnings("unused")
-        private String productType = "iPhone14,2";
-
-        private ClientContext() {
-            // empty to hide constructor
-        }
-
-        public static ClientContext defaultInstance() {
-            return new ClientContext();
-        }
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudFindMyDeviceRequest.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/request/ICloudFindMyDeviceRequest.java
deleted file mode 100644 (file)
index 7081ac3..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.request;
-
-import static org.openhab.binding.icloud.internal.ICloudBindingConstants.FIND_MY_DEVICE_REQUEST_SUBJECT;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Serializable class to create a "Find My Device" json request string.
- *
- * @author Patrik Gfeller - Initial Contribution
- */
-@NonNullByDefault
-public class ICloudFindMyDeviceRequest {
-    @SerializedName("device")
-    @Nullable
-    String deviceId;
-    final String subject = FIND_MY_DEVICE_REQUEST_SUBJECT;
-
-    public ICloudFindMyDeviceRequest(String id) {
-        deviceId = id;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountDataResponse.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountDataResponse.java
deleted file mode 100644 (file)
index 979f91a..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-import java.util.List;
-
-import com.google.gson.annotations.SerializedName;
-
-/**
- * Serializable class to parse the device information json response
- * received from the Apple server.
- *
- * @author Patrik Gfeller - Initial Contribution
- */
-public class ICloudAccountDataResponse {
-
-    @SerializedName("content")
-    private List<ICloudDeviceInformation> iCloudDeviceInformationList;
-
-    @SerializedName("serverContext")
-    private ICloudServerContext iCloudServerContext;
-
-    @SerializedName("statusCode")
-    private String iCloudAccountStatusCode;
-
-    @SerializedName("userInfo")
-    private ICloudAccountUserInfo iCloudAccountUserInfo;
-
-    public List<ICloudDeviceInformation> getICloudDeviceInformationList() {
-        return iCloudDeviceInformationList;
-    }
-
-    public String getICloudAccountStatusCode() {
-        return iCloudAccountStatusCode;
-    }
-
-    public ICloudAccountUserInfo getICloudAccountUserInfo() {
-        return iCloudAccountUserInfo;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountUserInfo.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudAccountUserInfo.java
deleted file mode 100644 (file)
index 80ffba7..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudAccountUserInfo {
-    private int accountFormatter;
-
-    private String firstName;
-
-    private boolean hasMembers;
-
-    private String lastName;
-
-    private Object membersInfo;
-
-    public int getAccountFormatter() {
-        return this.accountFormatter;
-    }
-
-    public String getFirstName() {
-        return this.firstName;
-    }
-
-    public boolean getHasMembers() {
-        return this.hasMembers;
-    }
-
-    public String getLastName() {
-        return this.lastName;
-    }
-
-    public Object getMembersInfo() {
-        return this.membersInfo;
-    }
-
-    public void setAccountFormatter(int accountFormatter) {
-        this.accountFormatter = accountFormatter;
-    }
-
-    public void setFirstName(String firstName) {
-        this.firstName = firstName;
-    }
-
-    public void setHasMembers(boolean hasMembers) {
-        this.hasMembers = hasMembers;
-    }
-
-    public void setLastName(String lastName) {
-        this.lastName = lastName;
-    }
-
-    public void setMembersInfo(Object membersInfo) {
-        this.membersInfo = membersInfo;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceFeatures.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceFeatures.java
deleted file mode 100644 (file)
index 39581a9..0000000
+++ /dev/null
@@ -1,153 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudDeviceFeatures {
-    private boolean CLK;
-
-    private boolean CLT;
-
-    private boolean CWP;
-
-    private boolean KEY;
-
-    private boolean KPD;
-
-    private boolean LCK;
-
-    private boolean LKL;
-
-    private boolean LKM;
-
-    private boolean LLC;
-
-    private boolean LMG;
-
-    private boolean LOC;
-
-    private boolean LST;
-
-    private boolean MCS;
-
-    private boolean MSG;
-
-    private boolean PIN;
-
-    private boolean REM;
-
-    private boolean SND;
-
-    private boolean SVP;
-
-    private boolean TEU;
-
-    private boolean WIP;
-
-    private boolean WMG;
-
-    private boolean XRM;
-
-    public boolean getCLK() {
-        return this.CLK;
-    }
-
-    public boolean getCLT() {
-        return this.CLT;
-    }
-
-    public boolean getCWP() {
-        return this.CWP;
-    }
-
-    public boolean getKEY() {
-        return this.KEY;
-    }
-
-    public boolean getKPD() {
-        return this.KPD;
-    }
-
-    public boolean getLCK() {
-        return this.LCK;
-    }
-
-    public boolean getLKL() {
-        return this.LKL;
-    }
-
-    public boolean getLKM() {
-        return this.LKM;
-    }
-
-    public boolean getLLC() {
-        return this.LLC;
-    }
-
-    public boolean getLMG() {
-        return this.LMG;
-    }
-
-    public boolean getLOC() {
-        return this.LOC;
-    }
-
-    public boolean getLST() {
-        return this.LST;
-    }
-
-    public boolean getMCS() {
-        return this.MCS;
-    }
-
-    public boolean getMSG() {
-        return this.MSG;
-    }
-
-    public boolean getPIN() {
-        return this.PIN;
-    }
-
-    public boolean getREM() {
-        return this.REM;
-    }
-
-    public boolean getSND() {
-        return this.SND;
-    }
-
-    public boolean getSVP() {
-        return this.SVP;
-    }
-
-    public boolean getTEU() {
-        return this.TEU;
-    }
-
-    public boolean getWIP() {
-        return this.WIP;
-    }
-
-    public boolean getWMG() {
-        return this.WMG;
-    }
-
-    public boolean getXRM() {
-        return this.XRM;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceInformation.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceInformation.java
deleted file mode 100644 (file)
index 6b5ca2e..0000000
+++ /dev/null
@@ -1,270 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-import java.util.ArrayList;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- * Contains device specific status information.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudDeviceInformation {
-    private boolean activationLocked;
-
-    private ArrayList<Object> audioChannels;
-
-    private double batteryLevel;
-
-    private String batteryStatus;
-
-    private boolean canWipeAfterLock;
-
-    private boolean darkWake;
-
-    private String deviceClass;
-
-    private String deviceColor;
-
-    private String deviceDisplayName;
-
-    private String deviceModel;
-
-    private int deviceStatus;
-
-    private ICloudDeviceFeatures features;
-
-    private boolean fmlyShare;
-
-    private String id;
-
-    private boolean isLocating;
-
-    private boolean isMac;
-
-    private ICloudDeviceLocation location;
-
-    private boolean locationCapable;
-
-    private boolean locationEnabled;
-
-    private boolean locFoundEnabled;
-
-    private Object lockedTimestamp;
-
-    private Object lostDevice;
-
-    private boolean lostModeCapable;
-
-    private boolean lostModeEnabled;
-
-    private String lostTimestamp;
-
-    private boolean lowPowerMode;
-
-    private int maxMsgChar;
-
-    private Object mesg;
-
-    private String modelDisplayName;
-
-    private Object msg;
-
-    private String name;
-
-    private int passcodeLength;
-
-    private String prsId;
-
-    private String rawDeviceModel;
-
-    private Object remoteLock;
-
-    private Object remoteWipe;
-
-    private Object snd;
-
-    private boolean thisDevice;
-
-    private Object trackingInfo;
-
-    private Object wipedTimestamp;
-
-    private boolean wipeInProgress;
-
-    public boolean getActivationLocked() {
-        return this.activationLocked;
-    }
-
-    public ArrayList<Object> getAudioChannels() {
-        return this.audioChannels;
-    }
-
-    public double getBatteryLevel() {
-        return this.batteryLevel;
-    }
-
-    public String getBatteryStatus() {
-        return this.batteryStatus;
-    }
-
-    public boolean getCanWipeAfterLock() {
-        return this.canWipeAfterLock;
-    }
-
-    public boolean getDarkWake() {
-        return this.darkWake;
-    }
-
-    public String getDeviceClass() {
-        return this.deviceClass;
-    }
-
-    public String getDeviceColor() {
-        return this.deviceColor;
-    }
-
-    public String getDeviceDisplayName() {
-        return this.deviceDisplayName;
-    }
-
-    public String getDeviceModel() {
-        return this.deviceModel;
-    }
-
-    public int getDeviceStatus() {
-        return this.deviceStatus;
-    }
-
-    public ICloudDeviceFeatures getFeatures() {
-        return this.features;
-    }
-
-    public boolean getFmlyShare() {
-        return this.fmlyShare;
-    }
-
-    public String getId() {
-        return this.id;
-    }
-
-    public boolean getIsLocating() {
-        return this.isLocating;
-    }
-
-    public boolean getIsMac() {
-        return this.isMac;
-    }
-
-    public ICloudDeviceLocation getLocation() {
-        return this.location;
-    }
-
-    public boolean getLocationCapable() {
-        return this.locationCapable;
-    }
-
-    public boolean getLocationEnabled() {
-        return this.locationEnabled;
-    }
-
-    public boolean getLocFoundEnabled() {
-        return this.locFoundEnabled;
-    }
-
-    public Object getLockedTimestamp() {
-        return this.lockedTimestamp;
-    }
-
-    public Object getLostDevice() {
-        return this.lostDevice;
-    }
-
-    public boolean getLostModeCapable() {
-        return this.lostModeCapable;
-    }
-
-    public boolean getLostModeEnabled() {
-        return this.lostModeEnabled;
-    }
-
-    public String getLostTimestamp() {
-        return this.lostTimestamp;
-    }
-
-    public boolean getLowPowerMode() {
-        return this.lowPowerMode;
-    }
-
-    public int getMaxMsgChar() {
-        return this.maxMsgChar;
-    }
-
-    public Object getMesg() {
-        return this.mesg;
-    }
-
-    public String getModelDisplayName() {
-        return this.modelDisplayName;
-    }
-
-    public Object getMsg() {
-        return this.msg;
-    }
-
-    public String getName() {
-        return this.name;
-    }
-
-    public int getPasscodeLength() {
-        return this.passcodeLength;
-    }
-
-    public String getPrsId() {
-        return this.prsId;
-    }
-
-    public String getRawDeviceModel() {
-        return this.rawDeviceModel;
-    }
-
-    public Object getRemoteLock() {
-        return this.remoteLock;
-    }
-
-    public Object getRemoteWipe() {
-        return this.remoteWipe;
-    }
-
-    public Object getSnd() {
-        return this.snd;
-    }
-
-    public boolean getThisDevice() {
-        return this.thisDevice;
-    }
-
-    public Object getTrackingInfo() {
-        return this.trackingInfo;
-    }
-
-    public Object getWipedTimestamp() {
-        return this.wipedTimestamp;
-    }
-
-    public boolean getWipeInProgress() {
-        return this.wipeInProgress;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceLocation.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudDeviceLocation.java
deleted file mode 100644 (file)
index 343183a..0000000
+++ /dev/null
@@ -1,94 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- * Contains device location information.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudDeviceLocation {
-    private double altitude;
-
-    private int floorLevel;
-
-    private double horizontalAccuracy;
-
-    private boolean isInaccurate;
-
-    private boolean isOld;
-
-    private double latitude;
-
-    private boolean locationFinished;
-
-    private String locationType;
-
-    private double longitude;
-
-    private String positionType;
-
-    private long timeStamp;
-
-    private double verticalAccuracy;
-
-    public double getAltitude() {
-        return this.altitude;
-    }
-
-    public int getFloorLevel() {
-        return this.floorLevel;
-    }
-
-    public double getHorizontalAccuracy() {
-        return this.horizontalAccuracy;
-    }
-
-    public boolean getIsInaccurate() {
-        return this.isInaccurate;
-    }
-
-    public boolean getIsOld() {
-        return this.isOld;
-    }
-
-    public double getLatitude() {
-        return this.latitude;
-    }
-
-    public boolean getLocationFinished() {
-        return this.locationFinished;
-    }
-
-    public String getLocationType() {
-        return this.locationType;
-    }
-
-    public double getLongitude() {
-        return this.longitude;
-    }
-
-    public String getPositionType() {
-        return this.positionType;
-    }
-
-    public long getTimeStamp() {
-        return this.timeStamp;
-    }
-
-    public double getVerticalAccuracy() {
-        return this.verticalAccuracy;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContext.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContext.java
deleted file mode 100644 (file)
index b695fef..0000000
+++ /dev/null
@@ -1,204 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudServerContext {
-    @Nullable
-    private String authToken;
-
-    private int callbackIntervalInMS;
-
-    private boolean classicUser;
-
-    private String clientId;
-
-    private boolean cloudUser;
-
-    private String deviceLoadStatus;
-
-    private boolean enable2FAErase;
-
-    private boolean enable2FAFamilyActions;
-
-    private boolean enable2FAFamilyRemove;
-
-    private boolean enableMapStats;
-
-    private String imageBaseUrl;
-
-    private String info;
-
-    private boolean isHSA;
-
-    private Object lastSessionExtensionTime;
-
-    private int macCount;
-
-    private int maxCallbackIntervalInMS;
-
-    private int maxDeviceLoadTime;
-
-    private int maxLocatingTime;
-
-    private int minCallbackIntervalInMS;
-
-    private int minTrackLocThresholdInMts;
-
-    private String preferredLanguage;
-
-    private long prefsUpdateTime;
-
-    private String prsId;
-
-    private long serverTimestamp;
-
-    private int sessionLifespan;
-
-    private boolean showSllNow;
-
-    private ICloudServerContextTimezone timezone;
-
-    private int trackInfoCacheDurationInSecs;
-
-    private boolean useAuthWidget;
-
-    private boolean validRegion;
-
-    public String getAuthToken() {
-        return this.authToken;
-    }
-
-    public int getCallbackIntervalInMS() {
-        return this.callbackIntervalInMS;
-    }
-
-    public boolean getClassicUser() {
-        return this.classicUser;
-    }
-
-    public String getClientId() {
-        return this.clientId;
-    }
-
-    public boolean getCloudUser() {
-        return this.cloudUser;
-    }
-
-    public String getDeviceLoadStatus() {
-        return this.deviceLoadStatus;
-    }
-
-    public boolean getEnable2FAErase() {
-        return this.enable2FAErase;
-    }
-
-    public boolean getEnable2FAFamilyActions() {
-        return this.enable2FAFamilyActions;
-    }
-
-    public boolean getEnable2FAFamilyRemove() {
-        return this.enable2FAFamilyRemove;
-    }
-
-    public boolean getEnableMapStats() {
-        return this.enableMapStats;
-    }
-
-    public String getImageBaseUrl() {
-        return this.imageBaseUrl;
-    }
-
-    public String getInfo() {
-        return this.info;
-    }
-
-    public boolean getIsHSA() {
-        return this.isHSA;
-    }
-
-    public Object getLastSessionExtensionTime() {
-        return this.lastSessionExtensionTime;
-    }
-
-    public int getMacCount() {
-        return this.macCount;
-    }
-
-    public int getMaxCallbackIntervalInMS() {
-        return this.maxCallbackIntervalInMS;
-    }
-
-    public int getMaxDeviceLoadTime() {
-        return this.maxDeviceLoadTime;
-    }
-
-    public int getMaxLocatingTime() {
-        return this.maxLocatingTime;
-    }
-
-    public int getMinCallbackIntervalInMS() {
-        return this.minCallbackIntervalInMS;
-    }
-
-    public int getMinTrackLocThresholdInMts() {
-        return this.minTrackLocThresholdInMts;
-    }
-
-    public String getPreferredLanguage() {
-        return this.preferredLanguage;
-    }
-
-    public long getPrefsUpdateTime() {
-        return this.prefsUpdateTime;
-    }
-
-    public String getPrsId() {
-        return this.prsId;
-    }
-
-    public long getServerTimestamp() {
-        return this.serverTimestamp;
-    }
-
-    public int getSessionLifespan() {
-        return this.sessionLifespan;
-    }
-
-    public boolean getShowSllNow() {
-        return this.showSllNow;
-    }
-
-    public ICloudServerContextTimezone getTimezone() {
-        return this.timezone;
-    }
-
-    public int getTrackInfoCacheDurationInSecs() {
-        return this.trackInfoCacheDurationInSecs;
-    }
-
-    public boolean getUseAuthWidget() {
-        return this.useAuthWidget;
-    }
-
-    public boolean getValidRegion() {
-        return this.validRegion;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContextTimezone.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/json/response/ICloudServerContextTimezone.java
deleted file mode 100644 (file)
index f5ce6e5..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.icloud.internal.json.response;
-
-/**
- * Serializable class to parse json response received from the Apple server.
- *
- * @author Patrik Gfeller - Initial Contribution
- *
- */
-public class ICloudServerContextTimezone {
-    private int currentOffset;
-
-    private int previousOffset;
-
-    private long previousTransition;
-
-    private String tzCurrentName;
-
-    private String tzName;
-
-    public int getCurrentOffset() {
-        return this.currentOffset;
-    }
-
-    public int getPreviousOffset() {
-        return this.previousOffset;
-    }
-
-    public long getPreviousTransition() {
-        return this.previousTransition;
-    }
-
-    public String getTzCurrentName() {
-        return this.tzCurrentName;
-    }
-
-    public String getTzName() {
-        return this.tzName;
-    }
-}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/CustomCookieStore.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/CustomCookieStore.java
new file mode 100644 (file)
index 0000000..a1a425f
--- /dev/null
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.utilities;
+
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This class implements a customized {@link CookieStore}. Its purpose is to add hyphens at the beginning and end of
+ * each cookie value which is required by Apple iCloud API.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+public class CustomCookieStore implements CookieStore {
+
+    private CookieStore cookieStore;
+
+    /**
+     * The constructor.
+     *
+     */
+    public CustomCookieStore() {
+        this.cookieStore = new CookieManager().getCookieStore();
+    }
+
+    @Override
+    public void add(@Nullable URI uri, @Nullable HttpCookie cookie) {
+        this.cookieStore.add(uri, cookie);
+    }
+
+    @Override
+    public @Nullable List<HttpCookie> get(@Nullable URI uri) {
+        List<HttpCookie> result = this.cookieStore.get(uri);
+        filterCookies(result);
+        return result;
+    }
+
+    @Override
+    public @Nullable List<HttpCookie> getCookies() {
+        List<HttpCookie> result = this.cookieStore.getCookies();
+        filterCookies(result);
+        return result;
+    }
+
+    @Override
+    public @Nullable List<URI> getURIs() {
+        return this.cookieStore.getURIs();
+    }
+
+    @Override
+    public boolean remove(@Nullable URI uri, @Nullable HttpCookie cookie) {
+        return this.cookieStore.remove(uri, cookie);
+    }
+
+    @Override
+    public boolean removeAll() {
+        return this.cookieStore.removeAll();
+    }
+
+    /**
+     * Add quotes add beginning and end of all cookie values
+     *
+     * @param cookieList Current cookies. This list is modified in-place.
+     */
+    private void filterCookies(List<HttpCookie> cookieList) {
+        for (HttpCookie cookie : cookieList) {
+            if (!cookie.getValue().startsWith("\"")) {
+                cookie.setValue("\"" + cookie.getValue());
+            }
+            if (!cookie.getValue().endsWith("\"")) {
+                cookie.setValue(cookie.getValue() + "\"");
+            }
+        }
+    }
+}
index f4026e13e75290225413ff258078e9b27a6bbda7..ec26d51a3cb26902c0e1fa0af56ceac2bbe1214b 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.icloud.internal.utilities;
 
 import java.util.Locale;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.core.i18n.LocaleProvider;
 import org.openhab.core.i18n.TranslationProvider;
@@ -24,6 +25,7 @@ import org.osgi.framework.Bundle;
  *
  * @author Patrik Gfeller - Initial contribution
  */
+@NonNullByDefault
 public class ICloudTextTranslator {
 
     private final Bundle bundle;
@@ -37,11 +39,13 @@ public class ICloudTextTranslator {
     }
 
     public String getText(String key, Object... arguments) {
-        Locale locale = localeProvider != null ? localeProvider.getLocale() : Locale.ENGLISH;
-        return i18nProvider != null ? i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments) : key;
+        Locale locale = localeProvider.getLocale();
+        String retText = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
+        return retText != null ? retText : key;
     }
 
     public String getDefaultText(@Nullable String key) {
-        return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
+        String retText = i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
+        return retText != null ? retText : key != null ? key : "UNKNOWN_TEXT";
     }
 }
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/JsonUtils.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/JsonUtils.java
new file mode 100644 (file)
index 0000000..dcde3d7
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.utilities;
+
+import java.lang.reflect.Type;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+import com.google.gson.reflect.TypeToken;
+
+/**
+ * Some helper method to ease and centralize use of GSON.
+ *
+ * @author Patrik Gfeller - Initial Contribution
+ * @author Simon Spielmann - Rename and generalization
+ *
+ */
+@NonNullByDefault
+public class JsonUtils {
+    private static final Gson GSON = new GsonBuilder().create();
+
+    private static final Type STRING_OBJ_MAP_TYPE = new TypeToken<Map<String, Object>>() {
+    }.getType();
+
+    /**
+     * Parse JSON to {@link Map}
+     *
+     * @param json JSON String or {@code null}.
+     * @return Parsed data or {@code null}
+     * @throws JsonSyntaxException If there is a JSON syntax error.
+     */
+    public static @Nullable Map<String, Object> toMap(@Nullable String json) throws JsonSyntaxException {
+        return GSON.fromJson(json, STRING_OBJ_MAP_TYPE);
+    }
+
+    /**
+     * Converts to JSON with {@link Gson}{@link #toJson(Object)}.
+     *
+     * @param data Data to convert.
+     * @return JSON representation of data.
+     */
+    public static @Nullable String toJson(@Nullable Object data) {
+        return GSON.toJson(data);
+    }
+
+    /**
+     * Defaults to {@link Gson#fromJson(String, Class)}.
+     *
+     * @param data Data to parse.
+     * @param classOfT Destination type
+     * @param <T> Destination type param
+     * @return Given type or {@code null}.
+     *
+     * @see Gson#fromJson(String, Class)
+     */
+    public static <@Nullable T> T fromJson(String data, Class<@NonNull T> classOfT) {
+        return GSON.fromJson(data, classOfT);
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/ListUtil.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/ListUtil.java
new file mode 100644 (file)
index 0000000..464f1b4
--- /dev/null
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.utilities;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * This class implements util methods for list handling.
+ *
+ * @author Simon Spielmann - Initial contribution
+ *
+ */
+@NonNullByDefault
+public abstract class ListUtil {
+
+    private ListUtil() {
+    };
+
+    /**
+     * Replace entries in the given originalList with entries from replacements, if the have an equal key.
+     *
+     * @param <K> Type of first pair element
+     * @param <V> Type of second pair element
+     * @param originalList List with entries to replace
+     * @param replacements Replacement entries
+     * @return New list with replaced entries
+     */
+    public static <K extends @NonNull Object, V extends @NonNull Object> List<Pair<K, V>> replaceEntries(
+            List<Pair<K, V>> originalList, @Nullable List<Pair<K, V>> replacements) {
+        List<Pair<K, V>> result = new ArrayList<>(originalList);
+        if (replacements != null) {
+            Iterator<Pair<K, V>> it = result.iterator();
+            while (it.hasNext()) {
+                Pair<K, V> requestHeader = it.next();
+                for (Pair<K, V> replacementHeader : replacements) {
+                    if (requestHeader.getKey().equals(replacementHeader.getKey())) {
+                        it.remove();
+                    }
+                }
+            }
+            result.addAll(replacements);
+        }
+        return result;
+    }
+}
diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/Pair.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/utilities/Pair.java
new file mode 100644 (file)
index 0000000..21dbc69
--- /dev/null
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud.internal.utilities;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Implementation of simple pair. Used mainly for HTTP header handling.
+ *
+ * @author Simon Spielmann - Initial contribution.
+ * @param <K> Type of first element
+ * @param <V> Type of second element
+ */
+@NonNullByDefault
+public class Pair<@NonNull K, @NonNull V> {
+
+    private K key;
+
+    private V value;
+
+    private Pair(K key, V value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    /**
+     * Create pair with key and value. Both of type {@link String}.
+     *
+     * @param key Key
+     * @param value Value
+     * @return Pair with given key and value
+     */
+    public static Pair<String, String> of(String key, String value) {
+        return new Pair<>(key, value);
+    }
+
+    @Override
+    public String toString() {
+        return "Pair [key=" + this.key + ", value=" + this.value + "]";
+    }
+
+    /**
+     * @return key
+     */
+    public K getKey() {
+        return this.key;
+    }
+
+    /**
+     * @return value
+     */
+    public V getValue() {
+        return this.value;
+    }
+}
index 43fc007e4b88e0b0dd1f0984f2754cd8a1077611..8ce78be768bb900af51dc611bd19889b9858b0fa 100644 (file)
@@ -10,6 +10,8 @@ icloud.account-thing.parameter.apple-id.label=Apple Id
 icloud.account-thing.parameter.apple-id.description=Apple Id (e-mail) to access iCloud information.
 icloud.account-thing.parameter.password.label=Password
 icloud.account-thing.parameter.password.description=Apple iCloud password for the given Id.
+icloud.account-thing.parameter.code.label=Code
+icloud.account-thing.parameter.code.description=Apple iCloud authentication code for 2-factor authentication.
 icloud.account-thing.parameter.refresh.label=Refresh
 icloud.account-thing.parameter.refresh.description=Refresh time in minutes.
 
index be950bed36ff4566e0dfe8ba6f6d1eba55346cff..0852433e1f78599e8a2ebc73ba4c2968b0e3bd10 100644 (file)
                                <label>@text/icloud.account-thing.parameter.password.label</label>
                                <description>@text/icloud.account-thing.parameter.password.description</description>
                        </parameter>
+                       <parameter name="code" type="text" required="false">
+                               <label>@text/icloud.account-thing.parameter.code.label</label>
+                               <description>@text/icloud.account-thing.parameter.code.description</description>
+                       </parameter>
                        <parameter name="refreshTimeInMinutes" type="integer" min="5" max="65535" unit="min">
                                <label>@text/icloud.account-thing.parameter.refresh.label</label>
                                <description>@text/icloud.account-thing.parameter.refresh.description</description>
diff --git a/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java b/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java
new file mode 100644 (file)
index 0000000..b4ee776
--- /dev/null
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.icloud;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.List;
+import java.util.Scanner;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
+import org.openhab.binding.icloud.internal.ICloudApiResponseException;
+import org.openhab.binding.icloud.internal.ICloudService;
+import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse;
+import org.openhab.binding.icloud.internal.utilities.JsonUtils;
+import org.openhab.core.storage.json.internal.JsonStorage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * Class to test/experiment with iCloud api.
+ *
+ * @author Simon Spielmann - Initial contribution
+ */
+@NonNullByDefault
+public class TestICloud {
+
+    private final String iCloudTestEmail;
+    private final String iCloudTestPassword;
+
+    private final Logger logger = LoggerFactory.getLogger(TestICloud.class);
+
+    @BeforeEach
+    private void setUp() {
+        final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
+        if (logger instanceof ch.qos.logback.classic.Logger) {
+            ((ch.qos.logback.classic.Logger) logger).setLevel(ch.qos.logback.classic.Level.DEBUG);
+        }
+    }
+
+    public TestICloud() {
+        String sysPropMail = System.getProperty("icloud.test.email");
+        String sysPropPW = System.getProperty("icloud.test.pw");
+        iCloudTestEmail = sysPropMail != null ? sysPropMail : "notset";
+        iCloudTestPassword = sysPropPW != null ? sysPropPW : "notset";
+    }
+
+    @Test
+    @EnabledIfSystemProperty(named = "icloud.test.email", matches = ".*", disabledReason = "Only for manual execution.")
+    public void testAuth() throws IOException, InterruptedException, ICloudApiResponseException, JsonSyntaxException {
+        File jsonStorageFile = new File(System.getProperty("user.home"), "openhab.json");
+        logger.info(jsonStorageFile.toString());
+
+        JsonStorage<String> stateStorage = new JsonStorage<String>(jsonStorageFile, TestICloud.class.getClassLoader(),
+                0, 1000, 1000, List.of());
+
+        ICloudService service = new ICloudService(iCloudTestEmail, iCloudTestPassword, stateStorage);
+        service.authenticate(false);
+        if (service.requires2fa()) {
+            PrintStream consoleOutput = System.out;
+            if (consoleOutput != null) {
+                consoleOutput.print("Code: ");
+            }
+            @SuppressWarnings("resource")
+            Scanner in = new Scanner(System.in);
+            String code = in.nextLine();
+            assertTrue(service.validate2faCode(code));
+            if (!service.isTrustedSession()) {
+                service.trustSession();
+            }
+            if (!service.isTrustedSession()) {
+                logger.info("Trust failed!!!");
+            }
+        }
+        ICloudAccountDataResponse deviceInfo = JsonUtils.fromJson(service.getDevices().refreshClient(),
+                ICloudAccountDataResponse.class);
+        assertNotNull(deviceInfo);
+        stateStorage.flush();
+    }
+}