mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[icloud] Rework authentication to reflect changes in iCloud API (#13691)
* Implement Authentication (WIP) * Validation Code accepted * Refactor session state * RefreshClient working * Implement session persistence in openhab store * Integration in binding * Remove persistent cookies, which break authentication * Bugfixing * Add code configuration to UI * Improve documentation, error-handling and cleanup * Rework auth order * Rework auth process * Add 2-FA-auth to documentation * Set bridge to online if data refresh works * Case-sensitive rename ICloudAPIResponseException * Include authentication in refresh flow * Fix regression for data not being updated * Fix typo in i18n props * Fix review and checkstyle. * More javadoc, new RetryException * Introduce @NonNullByDefault * Introduce server for RetryException, add NonNullbyDefault, fix warnings * Rework for contribution, e.g. null checks, ... * Fix checkstyle * Move JsonUtils to utilities package * Async initialize bridge handler. * Report Device OFFLINE if Bridge is OFFLINE * Set bridge thing status to UNKOWN in init * Move refresh init into async init * Cancel init task in dispose Also-by: Leo Siepel <leosiepel@gmail.com> Signed-off-by: Simon Spielmann <simon.spielmann@gmx.de>
This commit is contained in:
parent
6e8b35c4c1
commit
04f059c455
@ -29,6 +29,12 @@ The account Thing, more precisely the account Bridge, represents one Apple iClou
|
||||
The account can be connected to multiple Apple devices which are represented as Things below the Bridge, see the example below.
|
||||
You may create multiple account Things for multiple accounts.
|
||||
|
||||
If your Apple account has 2-factor-authentication enabled configuration requires two steps.
|
||||
First start by adding the Apple ID and password to your account thing configuration.
|
||||
You will receive a notification with a code on one of your Apple devices then.
|
||||
Add this code to the code parameter of the thing then and wait.
|
||||
The binding should be reinitialized and perform the authentication.
|
||||
|
||||
### Device Thing
|
||||
|
||||
A device is identified by the device ID provided by Apple.
|
||||
@ -62,7 +68,7 @@ The following channels are available (if supported by the device):
|
||||
### icloud.things
|
||||
|
||||
```php
|
||||
Bridge icloud:account:myaccount [appleId="mail@example.com", password="secure", refreshTimeInMinutes=5]
|
||||
Bridge icloud:account:myaccount [appleId="mail@example.com", password="secure", code="123456", refreshTimeInMinutes=5]
|
||||
{
|
||||
Thing device myiPhone8 "iPhone 8" @ "World" [deviceId="VIRG9FsrvXfE90ewVBA1H5swtwEQePdXVjHq3Si6pdJY2Cjro8QlreHYVGSUzuWV"]
|
||||
}
|
||||
@ -76,14 +82,14 @@ The information _@ "World"_ is optional.
|
||||
```php
|
||||
Group iCloud_Group "iPhone"
|
||||
|
||||
String iPhone_BatteryStatus "Battery Status [%s]" <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryStatus"}
|
||||
Number iPhone_BatteryLevel "Battery Level [%d %%]" <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryLevel"}
|
||||
Switch iPhone_FindMyPhone "Trigger Find My iPhone" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:findMyPhone", autoupdate="false"}
|
||||
Switch iPhone_Refresh "Force iPhone Refresh" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location", autoupdate="false"}
|
||||
Location iPhone_Location "Coordinates" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location"}
|
||||
Number iPhone_LocationAccuracy "Coordinates Accuracy [%.0f m]" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationAccuracy"}
|
||||
String iPhone_BatteryStatus "Battery Status [%s]" <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryStatus"}
|
||||
Number iPhone_BatteryLevel "Battery Level [%d %%]" <battery> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:batteryLevel"}
|
||||
Switch iPhone_FindMyPhone "Trigger Find My iPhone" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:findMyPhone", autoupdate="false"}
|
||||
Switch iPhone_Refresh "Force iPhone Refresh" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location", autoupdate="false"}
|
||||
Location iPhone_Location "Coordinates" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:location"}
|
||||
Number iPhone_LocationAccuracy "Coordinates Accuracy [%.0f m]" (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationAccuracy"}
|
||||
DateTime iPhone_LocationLastUpdate "Last Update [%1$td.%1$tm.%1$tY, %1$tH:%1$tM]" <time> (iCloud_Group) {channel="icloud:device:myaccount:myiPhone8:locationLastUpdate"}
|
||||
Switch iPhone_Home "Phone Home" <presence> (iCloud_Group)
|
||||
Switch iPhone_Home "Phone Home" <presence> (iCloud_Group)
|
||||
```
|
||||
|
||||
### icloud.sitemap
|
||||
|
@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
|
||||
/**
|
||||
* This class gives access to the find my iPhone (FMIP) service.
|
||||
*
|
||||
* @author Simon Spielmann - Initial Contribution.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FindMyIPhoneServiceManager {
|
||||
|
||||
private ICloudSession session;
|
||||
|
||||
private URI fmipRefreshUrl;
|
||||
|
||||
private URI fmipSoundUrl;
|
||||
|
||||
private static final String FMIP_ENDPOINT = "/fmipservice/client/web";
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param session {@link ICloudSession} to use for API calls.
|
||||
* @param serviceRoot Root URL for FMIP service.
|
||||
*/
|
||||
public FindMyIPhoneServiceManager(ICloudSession session, String serviceRoot) {
|
||||
this.session = session;
|
||||
this.fmipRefreshUrl = URI.create(serviceRoot + FMIP_ENDPOINT + "/refreshClient");
|
||||
this.fmipSoundUrl = URI.create(serviceRoot + FMIP_ENDPOINT + "/playSound");
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive client information as JSON.
|
||||
*
|
||||
* @return Information about all clients as JSON
|
||||
* {@link org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation}.
|
||||
*
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this blocking request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*
|
||||
*/
|
||||
public String refreshClient() throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
Map<String, Object> request = Map.of("clientContext",
|
||||
Map.of("fmly", true, "shouldLocate", true, "selectedDevice", "All", "deviceListVersion", 1));
|
||||
return session.post(this.fmipRefreshUrl.toString(), JsonUtils.toJson(request), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Play sound (find my iPhone) on given device.
|
||||
*
|
||||
* @param deviceId ID of the device to play sound on
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this blocking request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*/
|
||||
public void playSound(String deviceId) throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
Map<String, Object> request = Map.of("device", deviceId, "fmyl", true, "subject", "Message from openHAB.");
|
||||
session.post(this.fmipSoundUrl.toString(), JsonUtils.toJson(request), null);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
*
|
||||
* Exception for errors during calls of the iCloud API.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudApiResponseException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
private int statusCode;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param url URL for which the exception occurred
|
||||
* @param statusCode HTTP status code which was reported
|
||||
*/
|
||||
public ICloudApiResponseException(String url, int statusCode) {
|
||||
super(String.format("Request %s failed with %s.", url, statusCode));
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return statusCode HTTP status code of failed request.
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return this.statusCode;
|
||||
}
|
||||
}
|
@ -24,9 +24,8 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
* @author Patrik Gfeller
|
||||
* - Class renamed to be more consistent
|
||||
* - Constant FIND_MY_DEVICE_REQUEST_SUBJECT introduced
|
||||
* @author Patrik Gfeller - Class renamed to be more consistent
|
||||
* @author Patrik Gfeller - Constant FIND_MY_DEVICE_REQUEST_SUBJECT introduced
|
||||
* @author Gaël L'hopital - Added low battery
|
||||
*/
|
||||
@NonNullByDefault
|
||||
|
@ -1,93 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.icloud.internal.json.request.ICloudAccountDataRequest;
|
||||
import org.openhab.binding.icloud.internal.json.request.ICloudFindMyDeviceRequest;
|
||||
import org.openhab.core.io.net.http.HttpRequestBuilder;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
/**
|
||||
* Handles communication with the Apple server. Provides methods to
|
||||
* get device information and to find a device.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
* @author Patrik Gfeller - SOCKET_TIMEOUT changed from 2500 to 10000
|
||||
* @author Martin van Wingerden - add support for custom CA of https://fmipmobile.icloud.com
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudConnection {
|
||||
private static final String ICLOUD_URL = "https://www.icloud.com";
|
||||
private static final String ICLOUD_API_BASE_URL = "https://fmipmobile.icloud.com";
|
||||
private static final String ICLOUD_API_URL = ICLOUD_API_BASE_URL + "/fmipservice/device/";
|
||||
private static final String ICLOUD_API_COMMAND_PING_DEVICE = "/playSound";
|
||||
private static final String ICLOUD_API_COMMAND_REQUEST_DATA = "/initClient";
|
||||
private static final int SOCKET_TIMEOUT = 15;
|
||||
|
||||
private final Gson gson = new GsonBuilder().create();
|
||||
private final String iCloudDataRequest = gson.toJson(ICloudAccountDataRequest.defaultInstance());
|
||||
|
||||
private final String authorization;
|
||||
private final String iCloudDataRequestURL;
|
||||
private final String iCloudFindMyDeviceURL;
|
||||
|
||||
public ICloudConnection(String appleId, String password) throws URISyntaxException {
|
||||
authorization = new String(Base64.getEncoder().encode((appleId + ":" + password).getBytes()), UTF_8);
|
||||
iCloudDataRequestURL = new URI(ICLOUD_API_URL + appleId + ICLOUD_API_COMMAND_REQUEST_DATA).toASCIIString();
|
||||
iCloudFindMyDeviceURL = new URI(ICLOUD_API_URL + appleId + ICLOUD_API_COMMAND_PING_DEVICE).toASCIIString();
|
||||
}
|
||||
|
||||
/***
|
||||
* Sends a "find my device" request.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
public void findMyDevice(String id) throws IOException {
|
||||
callApi(iCloudFindMyDeviceURL, gson.toJson(new ICloudFindMyDeviceRequest(id)));
|
||||
}
|
||||
|
||||
public String requestDeviceStatusJSON() throws IOException {
|
||||
return callApi(iCloudDataRequestURL, iCloudDataRequest);
|
||||
}
|
||||
|
||||
private String callApi(String url, String payload) throws IOException {
|
||||
// @formatter:off
|
||||
return HttpRequestBuilder.postTo(url)
|
||||
.withTimeout(Duration.ofSeconds(SOCKET_TIMEOUT))
|
||||
.withHeader("Authorization", "Basic " + authorization)
|
||||
.withHeader("User-Agent", "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)")
|
||||
.withHeader("Origin", ICLOUD_URL)
|
||||
.withHeader("charset", "utf-8")
|
||||
.withHeader("Accept-language", "en-us")
|
||||
.withHeader("Connection", "keep-alive")
|
||||
.withHeader("X-Apple-Find-Api-Ver", "2.0")
|
||||
.withHeader("X-Apple-Authscheme", "UserIdGuest")
|
||||
.withHeader("X-Apple-Realm-Support", "1.0")
|
||||
.withHeader("X-Client-Name", "iPad")
|
||||
.withHeader("Content-Type", "application/json")
|
||||
.withContent(payload)
|
||||
.getContentAsString();
|
||||
// @formatter:on
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ package org.openhab.binding.icloud.internal;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
|
||||
|
||||
/**
|
||||
* Classes that implement this interface are interested in device information updates.
|
||||
|
@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudAccountDataResponse;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Extracts iCloud device information from a given JSON string
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudDeviceInformationParser {
|
||||
private final Gson gson = new GsonBuilder().create();
|
||||
|
||||
public @Nullable ICloudAccountDataResponse parse(String json) throws JsonSyntaxException {
|
||||
return gson.fromJson(json, ICloudAccountDataResponse.class);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery;
|
||||
import org.openhab.binding.icloud.internal.handler.ICloudAccountBridgeHandler;
|
||||
@ -25,6 +26,8 @@ import org.openhab.binding.icloud.internal.handler.ICloudDeviceHandler;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.openhab.core.storage.StorageService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
@ -33,21 +36,34 @@ import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* The {@link ICloudHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
* The {@link ICloudHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.icloud")
|
||||
@NonNullByDefault
|
||||
public class ICloudHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegistrations = new HashMap<>();
|
||||
|
||||
private LocaleProvider localeProvider;
|
||||
|
||||
private TranslationProvider i18nProvider;
|
||||
|
||||
private final StorageService storageService;
|
||||
|
||||
@Activate
|
||||
public ICloudHandlerFactory(@Reference StorageService storageService, @Reference LocaleProvider localeProvider,
|
||||
@Reference TranslationProvider i18nProvider) {
|
||||
this.storageService = storageService;
|
||||
this.localeProvider = localeProvider;
|
||||
this.i18nProvider = i18nProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
@ -58,7 +74,9 @@ public class ICloudHandlerFactory extends BaseThingHandlerFactory {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_ICLOUD)) {
|
||||
ICloudAccountBridgeHandler bridgeHandler = new ICloudAccountBridgeHandler((Bridge) thing);
|
||||
Storage<String> storage = this.storageService.getStorage(thing.getUID().toString(),
|
||||
String.class.getClassLoader());
|
||||
ICloudAccountBridgeHandler bridgeHandler = new ICloudAccountBridgeHandler((Bridge) thing, storage);
|
||||
registerDeviceDiscoveryService(bridgeHandler);
|
||||
return bridgeHandler;
|
||||
}
|
||||
@ -77,42 +95,24 @@ public class ICloudHandlerFactory extends BaseThingHandlerFactory {
|
||||
}
|
||||
|
||||
private synchronized void registerDeviceDiscoveryService(ICloudAccountBridgeHandler bridgeHandler) {
|
||||
ICloudDeviceDiscovery discoveryService = new ICloudDeviceDiscovery(bridgeHandler, bundleContext.getBundle(),
|
||||
i18nProvider, localeProvider);
|
||||
ICloudDeviceDiscovery discoveryService = new ICloudDeviceDiscovery(bridgeHandler,
|
||||
this.bundleContext.getBundle(), this.i18nProvider, this.localeProvider);
|
||||
discoveryService.activate();
|
||||
this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(),
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
|
||||
this.discoveryServiceRegistrations.put(bridgeHandler.getThing().getUID(), this.bundleContext
|
||||
.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
|
||||
}
|
||||
|
||||
private synchronized void unregisterDeviceDiscoveryService(ICloudAccountBridgeHandler bridgeHandler) {
|
||||
ServiceRegistration<?> serviceRegistration = this.discoveryServiceRegistrations
|
||||
.get(bridgeHandler.getThing().getUID());
|
||||
if (serviceRegistration != null) {
|
||||
ICloudDeviceDiscovery discoveryService = (ICloudDeviceDiscovery) bundleContext
|
||||
ICloudDeviceDiscovery discoveryService = (ICloudDeviceDiscovery) this.bundleContext
|
||||
.getService(serviceRegistration.getReference());
|
||||
if (discoveryService != null) {
|
||||
discoveryService.deactivate();
|
||||
}
|
||||
serviceRegistration.unregister();
|
||||
discoveryServiceRegistrations.remove(bridgeHandler.getThing().getUID());
|
||||
this.discoveryServiceRegistrations.remove(bridgeHandler.getThing().getUID());
|
||||
}
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setLocaleProvider(LocaleProvider localeProvider) {
|
||||
this.localeProvider = localeProvider;
|
||||
}
|
||||
|
||||
protected void unsetLocaleProvider(LocaleProvider localeProvider) {
|
||||
this.localeProvider = null;
|
||||
}
|
||||
|
||||
@Reference
|
||||
public void setTranslationProvider(TranslationProvider i18nProvider) {
|
||||
this.i18nProvider = i18nProvider;
|
||||
}
|
||||
|
||||
public void unsetTranslationProvider(TranslationProvider i18nProvider) {
|
||||
this.i18nProvider = null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,329 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
import org.openhab.binding.icloud.internal.utilities.ListUtil;
|
||||
import org.openhab.binding.icloud.internal.utilities.Pair;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class to access Apple iCloud API.
|
||||
*
|
||||
* The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudService {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private static final String ICLOUD_CLIENT_ID = "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ICloudService.class);
|
||||
|
||||
private static final String AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth";
|
||||
|
||||
private static final String HOME_ENDPOINT = "https://www.icloud.com";
|
||||
|
||||
private static final String SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1";
|
||||
|
||||
private String appleId;
|
||||
|
||||
private String password;
|
||||
|
||||
private String clientId;
|
||||
|
||||
private Map<String, Object> data = new HashMap<>();
|
||||
|
||||
private ICloudSession session;
|
||||
|
||||
/**
|
||||
*
|
||||
* The constructor.
|
||||
*
|
||||
* @param appleId Apple id (e-mail address) for authentication
|
||||
* @param password Password used for authentication
|
||||
* @param stateStorage Storage to save authentication state
|
||||
*/
|
||||
public ICloudService(String appleId, String password, Storage<String> stateStorage) {
|
||||
this.appleId = appleId;
|
||||
this.password = password;
|
||||
this.clientId = "auth-" + UUID.randomUUID().toString().toLowerCase();
|
||||
|
||||
this.session = new ICloudSession(stateStorage);
|
||||
this.session.setDefaultHeaders(Pair.of("Origin", HOME_ENDPOINT), Pair.of("Referer", HOME_ENDPOINT + "/"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate authentication
|
||||
*
|
||||
* @param forceRefresh Force a new authentication
|
||||
* @return {@code true} if authentication was successful
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if request was interrupted
|
||||
*/
|
||||
public boolean authenticate(boolean forceRefresh) throws IOException, InterruptedException {
|
||||
boolean loginSuccessful = false;
|
||||
if (this.session.getSessionToken() != null && !forceRefresh) {
|
||||
try {
|
||||
this.data = validateToken();
|
||||
logger.debug("Token is valid.");
|
||||
loginSuccessful = true;
|
||||
} catch (ICloudApiResponseException ex) {
|
||||
logger.debug("Token is not valid. Attemping new login.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (!loginSuccessful) {
|
||||
logger.debug("Authenticating as {}...", this.appleId);
|
||||
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("accountName", this.appleId);
|
||||
requestBody.put("password", this.password);
|
||||
requestBody.put("rememberMe", true);
|
||||
if (session.hasToken()) {
|
||||
requestBody.put("trustTokens", new String[] { this.session.getTrustToken() });
|
||||
} else {
|
||||
requestBody.put("trustTokens", new String[0]);
|
||||
}
|
||||
|
||||
List<Pair<String, String>> headers = getAuthHeaders();
|
||||
|
||||
try {
|
||||
this.session.post(AUTH_ENDPOINT + "/signin?isRememberMeEnabled=true", JsonUtils.toJson(requestBody),
|
||||
headers);
|
||||
} catch (ICloudApiResponseException ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return authenticateWithToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try authentication with stored session token. Returns {@code true} if authentication was successful.
|
||||
*
|
||||
* @return {@code true} if authentication was successful
|
||||
*
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this request was interrupted
|
||||
*
|
||||
*/
|
||||
public boolean authenticateWithToken() throws IOException, InterruptedException {
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
|
||||
String accountCountry = session.getAccountCountry();
|
||||
if (accountCountry != null) {
|
||||
requestBody.put("accountCountryCode", accountCountry);
|
||||
}
|
||||
|
||||
String sessionToken = session.getSessionToken();
|
||||
if (sessionToken != null) {
|
||||
requestBody.put("dsWebAuthToken", sessionToken);
|
||||
}
|
||||
|
||||
requestBody.put("extended_login", true);
|
||||
|
||||
if (session.hasToken()) {
|
||||
String token = session.getTrustToken();
|
||||
if (token != null) {
|
||||
requestBody.put("trustToken", token);
|
||||
}
|
||||
} else {
|
||||
requestBody.put("trustToken", "");
|
||||
}
|
||||
|
||||
try {
|
||||
@Nullable
|
||||
Map<String, Object> localSessionData = JsonUtils
|
||||
.toMap(session.post(SETUP_ENDPOINT + "/accountLogin", JsonUtils.toJson(requestBody), null));
|
||||
if (localSessionData != null) {
|
||||
data = localSessionData;
|
||||
}
|
||||
} catch (ICloudApiResponseException ex) {
|
||||
logger.debug("Invalid authentication.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param pair
|
||||
* @return
|
||||
*/
|
||||
private List<Pair<String, String>> getAuthHeaders() {
|
||||
return new ArrayList<>(List.of(Pair.of("Accept", "*/*"), Pair.of("Content-Type", "application/json"),
|
||||
Pair.of("X-Apple-OAuth-Client-Id", ICLOUD_CLIENT_ID),
|
||||
Pair.of("X-Apple-OAuth-Client-Type", "firstPartyAuth"),
|
||||
Pair.of("X-Apple-OAuth-Redirect-URI", HOME_ENDPOINT),
|
||||
Pair.of("X-Apple-OAuth-Require-Grant-Code", "true"),
|
||||
Pair.of("X-Apple-OAuth-Response-Mode", "web_message"), Pair.of("X-Apple-OAuth-Response-Type", "code"),
|
||||
Pair.of("X-Apple-OAuth-State", this.clientId), Pair.of("X-Apple-Widget-Key", ICLOUD_CLIENT_ID)));
|
||||
}
|
||||
|
||||
private Map<String, Object> validateToken() throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
logger.debug("Checking session token validity");
|
||||
String result = session.post(SETUP_ENDPOINT + "/validate", null, null);
|
||||
logger.debug("Session token is still valid");
|
||||
|
||||
@Nullable
|
||||
Map<String, Object> localSessionData = JsonUtils.toMap(result);
|
||||
if (localSessionData == null) {
|
||||
throw new IOException("Unable to create data object from json response");
|
||||
}
|
||||
return localSessionData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if 2-FA authentication is required.
|
||||
*
|
||||
* @return {@code true} if 2-FA authentication ({@link #validate2faCode(String)}) is required.
|
||||
*/
|
||||
public boolean requires2fa() {
|
||||
if (this.data.containsKey("dsInfo")) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> dsInfo = (@Nullable Map<String, Object>) this.data.get("dsInfo");
|
||||
if (dsInfo != null && ((Double) dsInfo.getOrDefault("hsaVersion", "0")) == 2.0) {
|
||||
return (this.data.containsKey("hsaChallengeRequired")
|
||||
&& ((Boolean) this.data.getOrDefault("hsaChallengeRequired", Boolean.FALSE)
|
||||
|| !isTrustedSession()));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if session is trusted.
|
||||
*
|
||||
* @return {@code true} if session is trusted. Call {@link #trustSession()} if not.
|
||||
*/
|
||||
public boolean isTrustedSession() {
|
||||
return (Boolean) this.data.getOrDefault("hsaTrustedBrowser", Boolean.FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides 2-FA code to establish trusted session.
|
||||
*
|
||||
* @param code Code given by user for 2-FA authentication.
|
||||
* @return {@code true} if code was accepted
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*/
|
||||
public boolean validate2faCode(String code) throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
Map<String, Object> requestBody = Map.of("securityCode", Map.of("code", code));
|
||||
|
||||
List<Pair<String, String>> headers = ListUtil.replaceEntries(getAuthHeaders(),
|
||||
List.of(Pair.of("Accept", "application/json")));
|
||||
|
||||
addSessionHeaders(headers);
|
||||
|
||||
try {
|
||||
this.session.post(AUTH_ENDPOINT + "/verify/trusteddevice/securitycode", JsonUtils.toJson(requestBody),
|
||||
headers);
|
||||
} catch (ICloudApiResponseException ex) {
|
||||
logger.debug("Code verification failed.", ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.debug("Code verification successful.");
|
||||
|
||||
trustSession();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addSessionHeaders(List<Pair<String, String>> headers) {
|
||||
String scnt = session.getScnt();
|
||||
if (scnt != null && !scnt.isEmpty()) {
|
||||
headers.add(Pair.of("scnt", scnt));
|
||||
}
|
||||
|
||||
String sessionId = session.getSessionId();
|
||||
if (sessionId != null && !sessionId.isEmpty()) {
|
||||
headers.add(Pair.of("X-Apple-ID-Session-Id", sessionId));
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable String getWebserviceUrl(String wsKey) {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> webservices = (@Nullable Map<String, Object>) data.get("webservices");
|
||||
if (webservices == null) {
|
||||
return null;
|
||||
}
|
||||
if (webservices.get(wsKey) instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, ?> wsMap = (@Nullable Map<String, ?>) webservices.get(wsKey);
|
||||
if (wsMap == null) {
|
||||
logger.error("Webservices result map has not expected format.");
|
||||
return null;
|
||||
}
|
||||
return (String) wsMap.get("url");
|
||||
} else {
|
||||
logger.error("Webservices result map has not expected format.");
|
||||
return null;
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
logger.error("ClassCastException, map has not expected format.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish trust for current session.
|
||||
*
|
||||
* @return {@code true} if successful.
|
||||
*
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*
|
||||
*/
|
||||
public boolean trustSession() throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
List<Pair<String, String>> headers = getAuthHeaders();
|
||||
|
||||
addSessionHeaders(headers);
|
||||
this.session.get(AUTH_ENDPOINT + "/2sv/trust", headers);
|
||||
return authenticateWithToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access to find my iPhone service.
|
||||
*
|
||||
* @return Instance of {@link FindMyIPhoneServiceManager} for this session.
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this request was interrupted
|
||||
*/
|
||||
public FindMyIPhoneServiceManager getDevices() throws IOException, InterruptedException {
|
||||
String webserviceUrl = getWebserviceUrl("findme");
|
||||
if (webserviceUrl != null) {
|
||||
return new FindMyIPhoneServiceManager(this.session, webserviceUrl);
|
||||
} else {
|
||||
throw new IllegalStateException("Webservice URLs not set. Need to authenticate first.");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,239 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Redirect;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpRequest.Builder;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.utilities.CustomCookieStore;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
import org.openhab.binding.icloud.internal.utilities.ListUtil;
|
||||
import org.openhab.binding.icloud.internal.utilities.Pair;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* Class to handle iCloud API session information for accessing the API.
|
||||
*
|
||||
* The implementation of this class is inspired by https://github.com/picklepete/pyicloud.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudSession {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ICloudSession.class);
|
||||
|
||||
private final HttpClient client;
|
||||
|
||||
private List<Pair<String, String>> headers = new ArrayList<>();
|
||||
|
||||
private ICloudSessionData data = new ICloudSessionData();
|
||||
|
||||
private Storage<String> stateStorage;
|
||||
|
||||
private static final String SESSION_DATA_KEY = "SESSION_DATA";
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param stateStorage Storage to persist session state.
|
||||
*/
|
||||
public ICloudSession(Storage<String> stateStorage) {
|
||||
String storedData = stateStorage.get(SESSION_DATA_KEY);
|
||||
if (storedData != null) {
|
||||
ICloudSessionData localSessionData = JsonUtils.fromJson(storedData, ICloudSessionData.class);
|
||||
if (localSessionData != null) {
|
||||
data = localSessionData;
|
||||
}
|
||||
}
|
||||
this.stateStorage = stateStorage;
|
||||
client = HttpClient.newBuilder().version(Version.HTTP_1_1).followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.cookieHandler(new CookieManager(new CustomCookieStore(), CookiePolicy.ACCEPT_ALL)).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an HTTP POST request to the given url and body.
|
||||
*
|
||||
* @param url URL to call.
|
||||
* @param body Body for the request
|
||||
* @param overrideHeaders If not null the given headers are used instead of the standard headers set via
|
||||
* {@link #setDefaultHeaders(Pair...)} (optional)
|
||||
* @return Result body as {@link String}.
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this blocking request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*/
|
||||
public String post(String url, @Nullable String body, @Nullable List<Pair<String, String>> overrideHeaders)
|
||||
throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
return request("POST", url, body, overrideHeaders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an HTTP GET request to the given url.
|
||||
*
|
||||
* @param url URL to call.
|
||||
* @param overrideHeaders If not null the given headers are used to replace corresponding entries of the standard
|
||||
* headers set via
|
||||
* {@link #setDefaultHeaders(Pair...)}
|
||||
* @return Result body as {@link String}.
|
||||
* @throws IOException if I/O error occurred
|
||||
* @throws InterruptedException if this blocking request was interrupted
|
||||
* @throws ICloudApiResponseException if the request failed (e.g. not OK HTTP return code)
|
||||
*/
|
||||
public String get(String url, List<Pair<String, String>> overrideHeaders)
|
||||
throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
return request("GET", url, null, overrideHeaders);
|
||||
}
|
||||
|
||||
private String request(String method, String url, @Nullable String body,
|
||||
@Nullable List<Pair<String, String>> overrideHeaders)
|
||||
throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
logger.debug("iCloud request {} {}.", method, url);
|
||||
|
||||
Builder builder = HttpRequest.newBuilder().uri(URI.create(url));
|
||||
|
||||
List<Pair<String, String>> requestHeaders = ListUtil.replaceEntries(this.headers, overrideHeaders);
|
||||
|
||||
for (Pair<String, String> header : requestHeaders) {
|
||||
builder.header(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
if (body != null) {
|
||||
builder.method(method, BodyPublishers.ofString(body));
|
||||
}
|
||||
|
||||
HttpRequest request = builder.build();
|
||||
|
||||
logger.trace("Calling {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, request.headers(), body);
|
||||
|
||||
HttpResponse<?> response = this.client.send(request, BodyHandlers.ofString());
|
||||
|
||||
Object responseBody = response.body();
|
||||
String responseBodyAsString = responseBody != null ? responseBody.toString() : "";
|
||||
|
||||
logger.trace("Result {} {}\nHeaders -----\n{}\nBody -----\n{}\n------\n", url, response.statusCode(),
|
||||
response.headers(), responseBodyAsString);
|
||||
|
||||
if (response.statusCode() >= 300) {
|
||||
throw new ICloudApiResponseException(url, response.statusCode());
|
||||
}
|
||||
|
||||
// Store headers to reuse authentication
|
||||
this.data.accountCountry = response.headers().firstValue("X-Apple-ID-Account-Country")
|
||||
.orElse(getAccountCountry());
|
||||
this.data.sessionId = response.headers().firstValue("X-Apple-ID-Session-Id").orElse(getSessionId());
|
||||
this.data.sessionToken = response.headers().firstValue("X-Apple-Session-Token").orElse(getSessionToken());
|
||||
this.data.trustToken = response.headers().firstValue("X-Apple-TwoSV-Trust-Token").orElse(getTrustToken());
|
||||
this.data.scnt = response.headers().firstValue("scnt").orElse(getScnt());
|
||||
|
||||
this.stateStorage.put(SESSION_DATA_KEY, JsonUtils.toJson(this.data));
|
||||
|
||||
return responseBodyAsString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default HTTP headers, for HTTP requests.
|
||||
*
|
||||
* @param headers HTTP headers to use for requests
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final void setDefaultHeaders(Pair<String, String>... headers) {
|
||||
this.headers = Arrays.asList(headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return scnt
|
||||
*/
|
||||
public @Nullable String getScnt() {
|
||||
return data.scnt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return sessionId
|
||||
*/
|
||||
public @Nullable String getSessionId() {
|
||||
return data.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return sessionToken
|
||||
*/
|
||||
public @Nullable String getSessionToken() {
|
||||
return data.sessionToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return trustToken
|
||||
*/
|
||||
public @Nullable String getTrustToken() {
|
||||
return data.trustToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if session token is not empty.
|
||||
*/
|
||||
public boolean hasToken() {
|
||||
String sessionToken = data.sessionToken;
|
||||
return sessionToken != null && !sessionToken.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return accountCountry
|
||||
*/
|
||||
public @Nullable String getAccountCountry() {
|
||||
return data.accountCountry;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Internal class to encapsulate data required for iCloud authentication.
|
||||
*
|
||||
* @author Simon Spielmann Initial Contribution
|
||||
*/
|
||||
private class ICloudSessionData {
|
||||
@Nullable
|
||||
String scnt;
|
||||
|
||||
@Nullable
|
||||
String sessionId;
|
||||
|
||||
@Nullable
|
||||
String sessionToken;
|
||||
|
||||
@Nullable
|
||||
String trustToken;
|
||||
|
||||
@Nullable
|
||||
String accountCountry;
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This exception is thrown when a retry finally fails.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RetryException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param originalException Exception which was thrown for the last unsuccessful retry.
|
||||
*/
|
||||
public RetryException(@Nullable Throwable originalException) {
|
||||
super("Retry finally failed.", originalException);
|
||||
}
|
||||
}
|
@ -26,4 +26,5 @@ public class ICloudAccountThingConfiguration {
|
||||
public @Nullable String appleId;
|
||||
public @Nullable String password;
|
||||
public int refreshTimeInMinutes = 10;
|
||||
public @Nullable String code;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ import java.util.List;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
|
||||
import org.openhab.binding.icloud.internal.handler.ICloudAccountBridgeHandler;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.utilities.ICloudTextTranslator;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.handler;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Enum to mark state during iCloud authentication.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum AuthState {
|
||||
|
||||
/**
|
||||
* Authentication was not tried yet.
|
||||
*/
|
||||
INITIAL,
|
||||
|
||||
/**
|
||||
* Entered credentials (apple id / password) are invalid.
|
||||
*/
|
||||
USER_PW_INVALID,
|
||||
|
||||
/**
|
||||
* Waiting for user to provide 2-FA code in thing configuration.
|
||||
*/
|
||||
WAIT_FOR_CODE,
|
||||
|
||||
/**
|
||||
* Sucessfully authenticated.
|
||||
*/
|
||||
AUTHENTICATED
|
||||
|
||||
}
|
@ -15,21 +15,26 @@ package org.openhab.binding.icloud.internal.handler;
|
||||
import static java.util.concurrent.TimeUnit.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.ICloudConnection;
|
||||
import org.openhab.binding.icloud.internal.ICloudApiResponseException;
|
||||
import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
|
||||
import org.openhab.binding.icloud.internal.ICloudDeviceInformationParser;
|
||||
import org.openhab.binding.icloud.internal.ICloudService;
|
||||
import org.openhab.binding.icloud.internal.RetryException;
|
||||
import org.openhab.binding.icloud.internal.configuration.ICloudAccountThingConfiguration;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudAccountDataResponse;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.storage.Storage;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
@ -37,19 +42,18 @@ import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseBridgeHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Retrieves the data for a given account from iCloud and passes the
|
||||
* information to {@link org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery} and to the
|
||||
* {@link ICloudDeviceHandler}s.
|
||||
* Retrieves the data for a given account from iCloud and passes the information to
|
||||
* {@link org.openhab.binding.icloud.internal.discovery.ICloudDeviceDiscovery} and to the {@link ICloudDeviceHandler}s.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
* @author Hans-Jörg Merk - Extended support with initial Contribution
|
||||
* @author Simon Spielmann - Rework for new iCloud API
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
@ -58,23 +62,36 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
private static final int CACHE_EXPIRY = (int) SECONDS.toMillis(10);
|
||||
|
||||
private final ICloudDeviceInformationParser deviceInformationParser = new ICloudDeviceInformationParser();
|
||||
private @Nullable ICloudConnection connection;
|
||||
private @Nullable ICloudService iCloudService;
|
||||
|
||||
private @Nullable ExpiringCache<String> iCloudDeviceInformationCache;
|
||||
|
||||
@Nullable
|
||||
ServiceRegistration<?> service;
|
||||
private AuthState authState = AuthState.INITIAL;
|
||||
|
||||
private final Object synchronizeRefresh = new Object();
|
||||
|
||||
private List<ICloudDeviceInformationListener> deviceInformationListeners = Collections
|
||||
.synchronizedList(new ArrayList<>());
|
||||
private Set<ICloudDeviceInformationListener> deviceInformationListeners = Collections
|
||||
.synchronizedSet(new HashSet<>());
|
||||
|
||||
@Nullable
|
||||
ScheduledFuture<?> refreshJob;
|
||||
|
||||
public ICloudAccountBridgeHandler(Bridge bridge) {
|
||||
@Nullable
|
||||
ScheduledFuture<?> initTask;
|
||||
|
||||
private Storage<String> storage;
|
||||
|
||||
private static final String AUTH_CODE_KEY = "AUTH_CODE";
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
* @param bridge The bridge to set
|
||||
* @param storage The storage service to set.
|
||||
*/
|
||||
public ICloudAccountBridgeHandler(Bridge bridge, Storage<String> storage) {
|
||||
super(bridge);
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -86,21 +103,161 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
public void initialize() {
|
||||
logger.debug("iCloud bridge handler initializing ...");
|
||||
iCloudDeviceInformationCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
|
||||
try {
|
||||
return connection.requestDeviceStatusJSON();
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unable to refresh device data", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
startHandler();
|
||||
logger.debug("iCloud bridge initialized.");
|
||||
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;
|
||||
}
|
||||
|
||||
do {
|
||||
try {
|
||||
if (authState == AuthState.AUTHENTICATED) {
|
||||
return wrapped.call();
|
||||
} else {
|
||||
checkLogin();
|
||||
}
|
||||
} catch (ICloudApiResponseException e) {
|
||||
logger.debug("ICloudApiResponseException with status code {}", e.getStatusCode());
|
||||
lastException = e;
|
||||
if (e.getStatusCode() == 450) {
|
||||
checkLogin();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
logger.debug("Need to authenticate first.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Wait for login");
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unable to refresh device data", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
logger.debug("Unexpected exception occured", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
try {
|
||||
Thread.sleep(200);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.interrupted();
|
||||
}
|
||||
} while (!success && retryCount < 3);
|
||||
throw new RetryException(lastException);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handle2FAAuthentication() throws IOException, InterruptedException, ICloudApiResponseException {
|
||||
logger.debug("Starting iCloud 2-FA authentication AuthState={}, Thing={})...", authState,
|
||||
getThing().getUID().getAsString());
|
||||
final ICloudService localICloudService = this.iCloudService;
|
||||
if (authState != AuthState.WAIT_FOR_CODE || localICloudService == null) {
|
||||
throw new IllegalStateException("2-FA authentication not initialized.");
|
||||
}
|
||||
ICloudAccountThingConfiguration config = getConfigAs(ICloudAccountThingConfiguration.class);
|
||||
String lastTriedCode = storage.get(AUTH_CODE_KEY);
|
||||
String code = config.code;
|
||||
boolean success = false;
|
||||
if (code == null || code.isBlank() || code.equals(lastTriedCode)) {
|
||||
// Still waiting for user to update config.
|
||||
logger.warn("ICloud authentication requires 2-FA code. Please provide code configuration for thing '{}'.",
|
||||
getThing().getUID().getAsString());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Please provide 2-FA code in thing configuration.");
|
||||
return false;
|
||||
} else {
|
||||
// 2-FA-Code was requested in previous call of this method.
|
||||
// User has provided code in config.
|
||||
logger.debug("Code is given in thing configuration '{}'. Trying to validate code...",
|
||||
getThing().getUID().getAsString());
|
||||
storage.put(AUTH_CODE_KEY, lastTriedCode);
|
||||
success = localICloudService.validate2faCode(code);
|
||||
if (!success) {
|
||||
authState = AuthState.INITIAL;
|
||||
logger.warn("ICloud token invalid.");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Invalid 2-FA-code.");
|
||||
return false;
|
||||
}
|
||||
org.openhab.core.config.core.Configuration config2 = editConfiguration();
|
||||
config2.put("code", "");
|
||||
updateConfiguration(config2);
|
||||
|
||||
logger.debug("Code is valid.");
|
||||
}
|
||||
authState = AuthState.AUTHENTICATED;
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
logger.debug("iCloud bridge handler '{}' authenticated with 2-FA code.", getThing().getUID().getAsString());
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,55 +267,126 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
if (refreshJob != null) {
|
||||
refreshJob.cancel(true);
|
||||
cancelRefresh();
|
||||
|
||||
final ScheduledFuture<?> localInitTask = this.initTask;
|
||||
if (localInitTask != null) {
|
||||
localInitTask.cancel(true);
|
||||
this.initTask = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public void findMyDevice(String deviceId) throws IOException {
|
||||
if (connection == null) {
|
||||
logger.debug("Can't send Find My Device request, because connection is null!");
|
||||
return;
|
||||
private void cancelRefresh() {
|
||||
final ScheduledFuture<?> localrefreshJob = this.refreshJob;
|
||||
if (localrefreshJob != null) {
|
||||
localrefreshJob.cancel(true);
|
||||
this.refreshJob = null;
|
||||
}
|
||||
connection.findMyDevice(deviceId);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
public void findMyDevice(String deviceId) throws IOException, InterruptedException {
|
||||
callApiWithRetryAndExceptionHandling(() -> {
|
||||
// callApiWithRetryAndExceptionHanlding ensures that iCloudService is not null when the following is
|
||||
// called. Cannot use method local iCloudService instance here, because instance may be replaced with a new
|
||||
// one during retry.
|
||||
iCloudService.getDevices().playSound(deviceId);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public void registerListener(ICloudDeviceInformationListener listener) {
|
||||
deviceInformationListeners.add(listener);
|
||||
this.deviceInformationListeners.add(listener);
|
||||
}
|
||||
|
||||
public void unregisterListener(ICloudDeviceInformationListener listener) {
|
||||
deviceInformationListeners.remove(listener);
|
||||
this.deviceInformationListeners.remove(listener);
|
||||
}
|
||||
|
||||
private void startHandler() {
|
||||
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 {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Apple ID/Password is not set!");
|
||||
return;
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::refreshData, 0, config.refreshTimeInMinutes, MINUTES);
|
||||
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.");
|
||||
}
|
||||
|
||||
logger.debug("iCloud bridge handler started.");
|
||||
} catch (URISyntaxException e) {
|
||||
logger.debug("Something went wrong while constructing the connection object", e);
|
||||
try {
|
||||
// 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,
|
||||
"Please provide 2-FA code in thing configuration.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!localICloudService.isTrustedSession()) {
|
||||
logger.debug("Trying to establish session trust.");
|
||||
success = localICloudService.trustSession();
|
||||
if (!success) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Session trust failed.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
authState = AuthState.AUTHENTICATED;
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
logger.debug("iCloud bridge handler authenticated.");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh iCloud device data.
|
||||
*/
|
||||
public void refreshData() {
|
||||
synchronized (synchronizeRefresh) {
|
||||
logger.debug("iCloud bridge refreshing data ...");
|
||||
|
||||
String json = iCloudDeviceInformationCache.getValue();
|
||||
logger.debug("iCloud bridge refreshing data ...");
|
||||
synchronized (this.synchronizeRefresh) {
|
||||
ExpiringCache<String> localCache = this.iCloudDeviceInformationCache;
|
||||
if (localCache == null) {
|
||||
return;
|
||||
}
|
||||
String json = localCache.getValue();
|
||||
logger.trace("json: {}", json);
|
||||
|
||||
if (json == null) {
|
||||
@ -166,7 +394,7 @@ public class ICloudAccountBridgeHandler extends BaseBridgeHandler {
|
||||
}
|
||||
|
||||
try {
|
||||
ICloudAccountDataResponse iCloudData = deviceInformationParser.parse(json);
|
||||
ICloudAccountDataResponse iCloudData = JsonUtils.fromJson(json, ICloudAccountDataResponse.class);
|
||||
if (iCloudData == null) {
|
||||
return;
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.icloud.internal.ICloudDeviceInformationListener;
|
||||
import org.openhab.binding.icloud.internal.configuration.ICloudDeviceThingConfiguration;
|
||||
import org.openhab.binding.icloud.internal.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudDeviceInformation;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
@ -36,6 +36,8 @@ import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
@ -51,13 +53,14 @@ import org.slf4j.LoggerFactory;
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
* @author Hans-Jörg Merk - Helped with testing and feedback
|
||||
* @author Gaël L'hopital - Added low battery
|
||||
* @author Simon Spielmann - Rework for new iCloud API
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDeviceInformationListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(ICloudDeviceHandler.class);
|
||||
|
||||
private @Nullable String deviceId;
|
||||
private @Nullable ICloudAccountBridgeHandler icloudAccount;
|
||||
|
||||
public ICloudDeviceHandler(Thing thing) {
|
||||
super(thing);
|
||||
@ -67,7 +70,7 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
|
||||
public void deviceInformationUpdate(List<ICloudDeviceInformation> deviceInformationList) {
|
||||
ICloudDeviceInformation deviceInformationRecord = getDeviceInformationRecord(deviceInformationList);
|
||||
if (deviceInformationRecord != null) {
|
||||
if (deviceInformationRecord.getDeviceStatus() == 200 || deviceInformationRecord.getDeviceStatus() == 203) {
|
||||
if (deviceInformationRecord.getDeviceStatus() == 200) {
|
||||
updateStatus(ONLINE);
|
||||
} else {
|
||||
updateStatus(OFFLINE, COMMUNICATION_ERROR, "Reported offline by iCloud webservice");
|
||||
@ -91,35 +94,42 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
Bridge bridge = getBridge();
|
||||
Object bridgeStatus = (bridge == null) ? null : bridge.getStatus();
|
||||
logger.debug("initializeThing thing [{}]; bridge status: [{}]", getThing().getUID(), bridgeStatus);
|
||||
|
||||
ICloudDeviceThingConfiguration configuration = getConfigAs(ICloudDeviceThingConfiguration.class);
|
||||
this.deviceId = configuration.deviceId;
|
||||
|
||||
ICloudAccountBridgeHandler handler = getIcloudAccount();
|
||||
if (handler != null) {
|
||||
refreshData();
|
||||
} else {
|
||||
updateStatus(OFFLINE, BRIDGE_UNINITIALIZED, "Bridge not found");
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshData() {
|
||||
ICloudAccountBridgeHandler bridge = getIcloudAccount();
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
bridge.refreshData();
|
||||
ICloudAccountBridgeHandler handler = (ICloudAccountBridgeHandler) bridge.getHandler();
|
||||
if (handler != null) {
|
||||
handler.registerListener(this);
|
||||
if (bridge.getStatus() == ThingStatus.ONLINE) {
|
||||
handler.refreshData();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED,
|
||||
"Bridge handler is not configured");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge is not configured");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
this.logger.trace("Command '{}' received for channel '{}'", command, channelUID);
|
||||
|
||||
ICloudAccountBridgeHandler bridge = getIcloudAccount();
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
logger.debug("No bridge found, ignoring command");
|
||||
this.logger.debug("No bridge found, ignoring command");
|
||||
return;
|
||||
}
|
||||
|
||||
ICloudAccountBridgeHandler bridgeHandler = (ICloudAccountBridgeHandler) bridge.getHandler();
|
||||
if (bridgeHandler == null) {
|
||||
this.logger.debug("No bridge handler found, ignoring command");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -129,27 +139,30 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
|
||||
try {
|
||||
final String deviceId = this.deviceId;
|
||||
if (deviceId == null) {
|
||||
logger.debug("Can't send Find My Device request, because deviceId is null!");
|
||||
this.logger.debug("Can't send Find My Device request, because deviceId is null!");
|
||||
return;
|
||||
}
|
||||
bridge.findMyDevice(deviceId);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Unable to execute find my device request", e);
|
||||
bridgeHandler.findMyDevice(deviceId);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
this.logger.warn("Unable to execute find my device request", e);
|
||||
}
|
||||
updateState(FIND_MY_PHONE, OnOffType.OFF);
|
||||
}
|
||||
}
|
||||
|
||||
if (command instanceof RefreshType) {
|
||||
bridge.refreshData();
|
||||
bridgeHandler.refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ICloudAccountBridgeHandler bridge = getIcloudAccount();
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
bridge.unregisterListener(this);
|
||||
ThingHandler bridgeHandler = bridge.getHandler();
|
||||
if (bridgeHandler instanceof ICloudAccountBridgeHandler) {
|
||||
((ICloudAccountBridgeHandler) bridgeHandler).unregisterListener(this);
|
||||
}
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
@ -169,7 +182,7 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
|
||||
|
||||
private @Nullable ICloudDeviceInformation getDeviceInformationRecord(
|
||||
List<ICloudDeviceInformation> deviceInformationList) {
|
||||
logger.debug("Device: [{}]", deviceId);
|
||||
this.logger.debug("Device: [{}]", this.deviceId);
|
||||
|
||||
for (ICloudDeviceInformation deviceInformationRecord : deviceInformationList) {
|
||||
String currentId = deviceInformationRecord.getId();
|
||||
@ -196,21 +209,4 @@ public class ICloudDeviceHandler extends BaseThingHandler implements ICloudDevic
|
||||
|
||||
return dateTime;
|
||||
}
|
||||
|
||||
private @Nullable ICloudAccountBridgeHandler getIcloudAccount() {
|
||||
if (icloudAccount == null) {
|
||||
Bridge bridge = getBridge();
|
||||
if (bridge == null) {
|
||||
return null;
|
||||
}
|
||||
ThingHandler handler = bridge.getHandler();
|
||||
if (handler instanceof ICloudAccountBridgeHandler) {
|
||||
icloudAccount = (ICloudAccountBridgeHandler) handler;
|
||||
icloudAccount.registerListener(this);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return icloudAccount;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
/**
|
||||
* Serializable class to parse json response received from the Apple server.
|
@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Serializable class to parse json response received from the Apple server.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
*
|
||||
*/
|
||||
public class ICloudDeviceFeatures {
|
||||
|
||||
@SerializedName("CLK")
|
||||
private boolean clk;
|
||||
|
||||
@SerializedName("CLT")
|
||||
private boolean clt;
|
||||
|
||||
@SerializedName("CWP")
|
||||
private boolean cwp;
|
||||
|
||||
@SerializedName("KEY")
|
||||
private boolean key;
|
||||
|
||||
@SerializedName("KPD")
|
||||
private boolean kpd;
|
||||
|
||||
@SerializedName("LCK")
|
||||
private boolean lck;
|
||||
|
||||
@SerializedName("LKL")
|
||||
private boolean lkl;
|
||||
|
||||
@SerializedName("LKM")
|
||||
private boolean lkm;
|
||||
|
||||
@SerializedName("LLC")
|
||||
private boolean llc;
|
||||
|
||||
@SerializedName("LMG")
|
||||
private boolean lmg;
|
||||
|
||||
@SerializedName("LOC")
|
||||
private boolean loc;
|
||||
|
||||
@SerializedName("LST")
|
||||
private boolean lst;
|
||||
|
||||
@SerializedName("MCS")
|
||||
private boolean mcs;
|
||||
|
||||
@SerializedName("MSG")
|
||||
private boolean msg;
|
||||
|
||||
@SerializedName("PIN")
|
||||
private boolean pin;
|
||||
|
||||
@SerializedName("REM")
|
||||
private boolean rem;
|
||||
|
||||
@SerializedName("SND")
|
||||
private boolean snd;
|
||||
|
||||
@SerializedName("SVP")
|
||||
private boolean svp;
|
||||
|
||||
@SerializedName("TEU")
|
||||
private boolean teu;
|
||||
|
||||
@SerializedName("WIP")
|
||||
private boolean wip;
|
||||
|
||||
@SerializedName("WMG")
|
||||
private boolean wmg;
|
||||
|
||||
@SerializedName("XRM")
|
||||
private boolean xrm;
|
||||
|
||||
@SerializedName("CLT")
|
||||
|
||||
public boolean getCLK() {
|
||||
return this.clk;
|
||||
}
|
||||
|
||||
public boolean getClt() {
|
||||
return this.clt;
|
||||
}
|
||||
|
||||
public boolean getCwp() {
|
||||
return this.cwp;
|
||||
}
|
||||
|
||||
public boolean getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public boolean getKpd() {
|
||||
return this.kpd;
|
||||
}
|
||||
|
||||
public boolean getLck() {
|
||||
return this.lck;
|
||||
}
|
||||
|
||||
public boolean getLkl() {
|
||||
return this.lkl;
|
||||
}
|
||||
|
||||
public boolean getLkm() {
|
||||
return this.lkm;
|
||||
}
|
||||
|
||||
public boolean getLlc() {
|
||||
return this.llc;
|
||||
}
|
||||
|
||||
public boolean getLmg() {
|
||||
return this.lmg;
|
||||
}
|
||||
|
||||
public boolean getLoc() {
|
||||
return this.loc;
|
||||
}
|
||||
|
||||
public boolean getLst() {
|
||||
return this.lst;
|
||||
}
|
||||
|
||||
public boolean getMcs() {
|
||||
return this.mcs;
|
||||
}
|
||||
|
||||
public boolean getMsg() {
|
||||
return this.msg;
|
||||
}
|
||||
|
||||
public boolean getPin() {
|
||||
return this.pin;
|
||||
}
|
||||
|
||||
public boolean getRem() {
|
||||
return this.rem;
|
||||
}
|
||||
|
||||
public boolean getSnd() {
|
||||
return this.snd;
|
||||
}
|
||||
|
||||
public boolean getSvp() {
|
||||
return this.svp;
|
||||
}
|
||||
|
||||
public boolean getTeu() {
|
||||
return this.teu;
|
||||
}
|
||||
|
||||
public boolean getWip() {
|
||||
return this.wip;
|
||||
}
|
||||
|
||||
public boolean getWmg() {
|
||||
return this.wmg;
|
||||
}
|
||||
|
||||
public boolean getXrm() {
|
||||
return this.xrm;
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
/**
|
||||
* Serializable class to parse json response received from the Apple server.
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
@ -10,7 +10,7 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
package org.openhab.binding.icloud.internal.handler.dto.json.response;
|
||||
|
||||
/**
|
||||
* Serializable class to parse json response received from the Apple server.
|
@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.request;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Serializable request for icloud device data.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudAccountDataRequest {
|
||||
@SuppressWarnings("unused")
|
||||
private ClientContext clientContext;
|
||||
|
||||
private ICloudAccountDataRequest() {
|
||||
this.clientContext = ClientContext.defaultInstance();
|
||||
}
|
||||
|
||||
public static ICloudAccountDataRequest defaultInstance() {
|
||||
return new ICloudAccountDataRequest();
|
||||
}
|
||||
|
||||
public static class ClientContext {
|
||||
@SuppressWarnings("unused")
|
||||
private String appName = "FindMyiPhone";
|
||||
@SuppressWarnings("unused")
|
||||
private boolean fmly = true;
|
||||
@SuppressWarnings("unused")
|
||||
private String appVersion = "5.0";
|
||||
@SuppressWarnings("unused")
|
||||
private String buildVersion = "376";
|
||||
@SuppressWarnings("unused")
|
||||
private int clientTimestamp = 0;
|
||||
@SuppressWarnings("unused")
|
||||
private String deviceUDID = "";
|
||||
@SuppressWarnings("unused")
|
||||
private int inactiveTime = 1;
|
||||
@SuppressWarnings("unused")
|
||||
private String osVersion = "14.0";
|
||||
@SuppressWarnings("unused")
|
||||
private String productType = "iPhone14,2";
|
||||
|
||||
private ClientContext() {
|
||||
// empty to hide constructor
|
||||
}
|
||||
|
||||
public static ClientContext defaultInstance() {
|
||||
return new ClientContext();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.request;
|
||||
|
||||
import static org.openhab.binding.icloud.internal.ICloudBindingConstants.FIND_MY_DEVICE_REQUEST_SUBJECT;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Serializable class to create a "Find My Device" json request string.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudFindMyDeviceRequest {
|
||||
@SerializedName("device")
|
||||
@Nullable
|
||||
String deviceId;
|
||||
final String subject = FIND_MY_DEVICE_REQUEST_SUBJECT;
|
||||
|
||||
public ICloudFindMyDeviceRequest(String id) {
|
||||
deviceId = id;
|
||||
}
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.json.response;
|
||||
|
||||
/**
|
||||
* Serializable class to parse json response received from the Apple server.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
*
|
||||
*/
|
||||
public class ICloudDeviceFeatures {
|
||||
private boolean CLK;
|
||||
|
||||
private boolean CLT;
|
||||
|
||||
private boolean CWP;
|
||||
|
||||
private boolean KEY;
|
||||
|
||||
private boolean KPD;
|
||||
|
||||
private boolean LCK;
|
||||
|
||||
private boolean LKL;
|
||||
|
||||
private boolean LKM;
|
||||
|
||||
private boolean LLC;
|
||||
|
||||
private boolean LMG;
|
||||
|
||||
private boolean LOC;
|
||||
|
||||
private boolean LST;
|
||||
|
||||
private boolean MCS;
|
||||
|
||||
private boolean MSG;
|
||||
|
||||
private boolean PIN;
|
||||
|
||||
private boolean REM;
|
||||
|
||||
private boolean SND;
|
||||
|
||||
private boolean SVP;
|
||||
|
||||
private boolean TEU;
|
||||
|
||||
private boolean WIP;
|
||||
|
||||
private boolean WMG;
|
||||
|
||||
private boolean XRM;
|
||||
|
||||
public boolean getCLK() {
|
||||
return this.CLK;
|
||||
}
|
||||
|
||||
public boolean getCLT() {
|
||||
return this.CLT;
|
||||
}
|
||||
|
||||
public boolean getCWP() {
|
||||
return this.CWP;
|
||||
}
|
||||
|
||||
public boolean getKEY() {
|
||||
return this.KEY;
|
||||
}
|
||||
|
||||
public boolean getKPD() {
|
||||
return this.KPD;
|
||||
}
|
||||
|
||||
public boolean getLCK() {
|
||||
return this.LCK;
|
||||
}
|
||||
|
||||
public boolean getLKL() {
|
||||
return this.LKL;
|
||||
}
|
||||
|
||||
public boolean getLKM() {
|
||||
return this.LKM;
|
||||
}
|
||||
|
||||
public boolean getLLC() {
|
||||
return this.LLC;
|
||||
}
|
||||
|
||||
public boolean getLMG() {
|
||||
return this.LMG;
|
||||
}
|
||||
|
||||
public boolean getLOC() {
|
||||
return this.LOC;
|
||||
}
|
||||
|
||||
public boolean getLST() {
|
||||
return this.LST;
|
||||
}
|
||||
|
||||
public boolean getMCS() {
|
||||
return this.MCS;
|
||||
}
|
||||
|
||||
public boolean getMSG() {
|
||||
return this.MSG;
|
||||
}
|
||||
|
||||
public boolean getPIN() {
|
||||
return this.PIN;
|
||||
}
|
||||
|
||||
public boolean getREM() {
|
||||
return this.REM;
|
||||
}
|
||||
|
||||
public boolean getSND() {
|
||||
return this.SND;
|
||||
}
|
||||
|
||||
public boolean getSVP() {
|
||||
return this.SVP;
|
||||
}
|
||||
|
||||
public boolean getTEU() {
|
||||
return this.TEU;
|
||||
}
|
||||
|
||||
public boolean getWIP() {
|
||||
return this.WIP;
|
||||
}
|
||||
|
||||
public boolean getWMG() {
|
||||
return this.WMG;
|
||||
}
|
||||
|
||||
public boolean getXRM() {
|
||||
return this.XRM;
|
||||
}
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.utilities;
|
||||
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookieStore;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class implements a customized {@link CookieStore}. Its purpose is to add hyphens at the beginning and end of
|
||||
* each cookie value which is required by Apple iCloud API.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
public class CustomCookieStore implements CookieStore {
|
||||
|
||||
private CookieStore cookieStore;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*
|
||||
*/
|
||||
public CustomCookieStore() {
|
||||
this.cookieStore = new CookieManager().getCookieStore();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(@Nullable URI uri, @Nullable HttpCookie cookie) {
|
||||
this.cookieStore.add(uri, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<HttpCookie> get(@Nullable URI uri) {
|
||||
List<HttpCookie> result = this.cookieStore.get(uri);
|
||||
filterCookies(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<HttpCookie> getCookies() {
|
||||
List<HttpCookie> result = this.cookieStore.getCookies();
|
||||
filterCookies(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<URI> getURIs() {
|
||||
return this.cookieStore.getURIs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(@Nullable URI uri, @Nullable HttpCookie cookie) {
|
||||
return this.cookieStore.remove(uri, cookie);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll() {
|
||||
return this.cookieStore.removeAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add quotes add beginning and end of all cookie values
|
||||
*
|
||||
* @param cookieList Current cookies. This list is modified in-place.
|
||||
*/
|
||||
private void filterCookies(List<HttpCookie> cookieList) {
|
||||
for (HttpCookie cookie : cookieList) {
|
||||
if (!cookie.getValue().startsWith("\"")) {
|
||||
cookie.setValue("\"" + cookie.getValue());
|
||||
}
|
||||
if (!cookie.getValue().endsWith("\"")) {
|
||||
cookie.setValue(cookie.getValue() + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ package org.openhab.binding.icloud.internal.utilities;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.core.i18n.TranslationProvider;
|
||||
@ -24,6 +25,7 @@ import org.osgi.framework.Bundle;
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ICloudTextTranslator {
|
||||
|
||||
private final Bundle bundle;
|
||||
@ -37,11 +39,13 @@ public class ICloudTextTranslator {
|
||||
}
|
||||
|
||||
public String getText(String key, Object... arguments) {
|
||||
Locale locale = localeProvider != null ? localeProvider.getLocale() : Locale.ENGLISH;
|
||||
return i18nProvider != null ? i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments) : key;
|
||||
Locale locale = localeProvider.getLocale();
|
||||
String retText = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
|
||||
return retText != null ? retText : key;
|
||||
}
|
||||
|
||||
public String getDefaultText(@Nullable String key) {
|
||||
return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
|
||||
String retText = i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
|
||||
return retText != null ? retText : key != null ? key : "UNKNOWN_TEXT";
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.utilities;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
/**
|
||||
* Some helper method to ease and centralize use of GSON.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial Contribution
|
||||
* @author Simon Spielmann - Rename and generalization
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class JsonUtils {
|
||||
private static final Gson GSON = new GsonBuilder().create();
|
||||
|
||||
private static final Type STRING_OBJ_MAP_TYPE = new TypeToken<Map<String, Object>>() {
|
||||
}.getType();
|
||||
|
||||
/**
|
||||
* Parse JSON to {@link Map}
|
||||
*
|
||||
* @param json JSON String or {@code null}.
|
||||
* @return Parsed data or {@code null}
|
||||
* @throws JsonSyntaxException If there is a JSON syntax error.
|
||||
*/
|
||||
public static @Nullable Map<String, Object> toMap(@Nullable String json) throws JsonSyntaxException {
|
||||
return GSON.fromJson(json, STRING_OBJ_MAP_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to JSON with {@link Gson}{@link #toJson(Object)}.
|
||||
*
|
||||
* @param data Data to convert.
|
||||
* @return JSON representation of data.
|
||||
*/
|
||||
public static @Nullable String toJson(@Nullable Object data) {
|
||||
return GSON.toJson(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to {@link Gson#fromJson(String, Class)}.
|
||||
*
|
||||
* @param data Data to parse.
|
||||
* @param classOfT Destination type
|
||||
* @param <T> Destination type param
|
||||
* @return Given type or {@code null}.
|
||||
*
|
||||
* @see Gson#fromJson(String, Class)
|
||||
*/
|
||||
public static <@Nullable T> T fromJson(String data, Class<@NonNull T> classOfT) {
|
||||
return GSON.fromJson(data, classOfT);
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.utilities;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class implements util methods for list handling.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class ListUtil {
|
||||
|
||||
private ListUtil() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace entries in the given originalList with entries from replacements, if the have an equal key.
|
||||
*
|
||||
* @param <K> Type of first pair element
|
||||
* @param <V> Type of second pair element
|
||||
* @param originalList List with entries to replace
|
||||
* @param replacements Replacement entries
|
||||
* @return New list with replaced entries
|
||||
*/
|
||||
public static <K extends @NonNull Object, V extends @NonNull Object> List<Pair<K, V>> replaceEntries(
|
||||
List<Pair<K, V>> originalList, @Nullable List<Pair<K, V>> replacements) {
|
||||
List<Pair<K, V>> result = new ArrayList<>(originalList);
|
||||
if (replacements != null) {
|
||||
Iterator<Pair<K, V>> it = result.iterator();
|
||||
while (it.hasNext()) {
|
||||
Pair<K, V> requestHeader = it.next();
|
||||
for (Pair<K, V> replacementHeader : replacements) {
|
||||
if (requestHeader.getKey().equals(replacementHeader.getKey())) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
result.addAll(replacements);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud.internal.utilities;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Implementation of simple pair. Used mainly for HTTP header handling.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution.
|
||||
* @param <K> Type of first element
|
||||
* @param <V> Type of second element
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Pair<@NonNull K, @NonNull V> {
|
||||
|
||||
private K key;
|
||||
|
||||
private V value;
|
||||
|
||||
private Pair(K key, V value) {
|
||||
this.key = key;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create pair with key and value. Both of type {@link String}.
|
||||
*
|
||||
* @param key Key
|
||||
* @param value Value
|
||||
* @return Pair with given key and value
|
||||
*/
|
||||
public static Pair<String, String> of(String key, String value) {
|
||||
return new Pair<>(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Pair [key=" + this.key + ", value=" + this.value + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return key
|
||||
*/
|
||||
public K getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return value
|
||||
*/
|
||||
public V getValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
@ -10,6 +10,8 @@ icloud.account-thing.parameter.apple-id.label=Apple Id
|
||||
icloud.account-thing.parameter.apple-id.description=Apple Id (e-mail) to access iCloud information.
|
||||
icloud.account-thing.parameter.password.label=Password
|
||||
icloud.account-thing.parameter.password.description=Apple iCloud password for the given Id.
|
||||
icloud.account-thing.parameter.code.label=Code
|
||||
icloud.account-thing.parameter.code.description=Apple iCloud authentication code for 2-factor authentication.
|
||||
icloud.account-thing.parameter.refresh.label=Refresh
|
||||
icloud.account-thing.parameter.refresh.description=Refresh time in minutes.
|
||||
|
||||
|
@ -18,6 +18,10 @@
|
||||
<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>
|
||||
|
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.icloud;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
|
||||
import org.openhab.binding.icloud.internal.ICloudApiResponseException;
|
||||
import org.openhab.binding.icloud.internal.ICloudService;
|
||||
import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse;
|
||||
import org.openhab.binding.icloud.internal.utilities.JsonUtils;
|
||||
import org.openhab.core.storage.json.internal.JsonStorage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
/**
|
||||
* Class to test/experiment with iCloud api.
|
||||
*
|
||||
* @author Simon Spielmann - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TestICloud {
|
||||
|
||||
private final String iCloudTestEmail;
|
||||
private final String iCloudTestPassword;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(TestICloud.class);
|
||||
|
||||
@BeforeEach
|
||||
private void setUp() {
|
||||
final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
|
||||
if (logger instanceof ch.qos.logback.classic.Logger) {
|
||||
((ch.qos.logback.classic.Logger) logger).setLevel(ch.qos.logback.classic.Level.DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
public TestICloud() {
|
||||
String sysPropMail = System.getProperty("icloud.test.email");
|
||||
String sysPropPW = System.getProperty("icloud.test.pw");
|
||||
iCloudTestEmail = sysPropMail != null ? sysPropMail : "notset";
|
||||
iCloudTestPassword = sysPropPW != null ? sysPropPW : "notset";
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIfSystemProperty(named = "icloud.test.email", matches = ".*", disabledReason = "Only for manual execution.")
|
||||
public void testAuth() throws IOException, InterruptedException, ICloudApiResponseException, JsonSyntaxException {
|
||||
File jsonStorageFile = new File(System.getProperty("user.home"), "openhab.json");
|
||||
logger.info(jsonStorageFile.toString());
|
||||
|
||||
JsonStorage<String> stateStorage = new JsonStorage<String>(jsonStorageFile, TestICloud.class.getClassLoader(),
|
||||
0, 1000, 1000, List.of());
|
||||
|
||||
ICloudService service = new ICloudService(iCloudTestEmail, iCloudTestPassword, stateStorage);
|
||||
service.authenticate(false);
|
||||
if (service.requires2fa()) {
|
||||
PrintStream consoleOutput = System.out;
|
||||
if (consoleOutput != null) {
|
||||
consoleOutput.print("Code: ");
|
||||
}
|
||||
@SuppressWarnings("resource")
|
||||
Scanner in = new Scanner(System.in);
|
||||
String code = in.nextLine();
|
||||
assertTrue(service.validate2faCode(code));
|
||||
if (!service.isTrustedSession()) {
|
||||
service.trustSession();
|
||||
}
|
||||
if (!service.isTrustedSession()) {
|
||||
logger.info("Trust failed!!!");
|
||||
}
|
||||
}
|
||||
ICloudAccountDataResponse deviceInfo = JsonUtils.fromJson(service.getDevices().refreshClient(),
|
||||
ICloudAccountDataResponse.class);
|
||||
assertNotNull(deviceInfo);
|
||||
stateStorage.flush();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user