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.
### 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"]
}
```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
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
* 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
+++ /dev/null
-/**
- * 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
- }
-}
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.
+++ /dev/null
-/**
- * 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);
- }
-}
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;
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;
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);
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;
}
}
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;
- }
}
--- /dev/null
+/**
+ * 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.");
+ }
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
public @Nullable String appleId;
public @Nullable String password;
public int refreshTimeInMinutes = 10;
+ public @Nullable String code;
}
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;
--- /dev/null
+/**
+ * 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
+
+}
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;
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 {
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
}
}
+ @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
@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) {
}
try {
- ICloudAccountDataResponse iCloudData = deviceInformationParser.parse(json);
+ ICloudAccountDataResponse iCloudData = JsonUtils.fromJson(json, ICloudAccountDataResponse.class);
if (iCloudData == null) {
return;
}
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;
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;
* @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);
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");
@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;
}
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();
}
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();
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;
- }
}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
+++ /dev/null
-/**
- * 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();
- }
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
--- /dev/null
+/**
+ * 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() + "\"");
+ }
+ }
+ }
+}
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;
*
* @author Patrik Gfeller - Initial contribution
*/
+@NonNullByDefault
public class ICloudTextTranslator {
private final Bundle bundle;
}
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";
}
}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
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.
<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>
--- /dev/null
+/**
+ * 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();
+ }
+}