diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/discovery/ICloudDeviceDiscovery.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/discovery/ICloudDeviceDiscovery.java index 429795bf584d9..82e6f12a7aeaf 100644 --- a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/discovery/ICloudDeviceDiscovery.java +++ b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/discovery/ICloudDeviceDiscovery.java @@ -63,19 +63,23 @@ public void deviceInformationUpdate(List deviceInformat String deviceOwnerName = deviceInformationRecord.getName(); String thingLabel = deviceOwnerName + " (" + deviceTypeName + ")"; - String deviceId = deviceInformationRecord.getId(); - String deviceIdHash = Integer.toHexString(deviceId.hashCode()); + String deviceDiscoveryId = deviceInformationRecord.getDeviceDiscoveryId(); + if (deviceDiscoveryId == null || deviceDiscoveryId.isBlank()) { + logger.debug("deviceDiscoveryId is empty, using device name for identification."); + deviceDiscoveryId = deviceOwnerName; + } + String deviceIdHash = Integer.toHexString(deviceDiscoveryId.hashCode()); logger.debug("iCloud device discovery for [{}]", deviceInformationRecord.getDeviceDisplayName()); ThingUID uid = new ThingUID(THING_TYPE_ICLOUDDEVICE, bridgeUID, deviceIdHash); DiscoveryResult result = DiscoveryResultBuilder.create(uid).withBridge(bridgeUID) - .withProperty(DEVICE_PROPERTY_ID, deviceId) - .withProperty(translatorService.getText(DEVICE_PROPERTY_ID_LABEL), deviceId) + .withProperty(DEVICE_PROPERTY_ID, deviceDiscoveryId) + .withProperty(translatorService.getText(DEVICE_PROPERTY_ID_LABEL), deviceDiscoveryId) .withProperty(translatorService.getText(DEVICE_PROPERTY_OWNER_LABEL), deviceOwnerName) .withRepresentationProperty(DEVICE_PROPERTY_ID).withLabel(thingLabel).build(); - logger.debug("Device [{}, {}] found.", deviceIdHash, deviceId); + logger.debug("Device [{}, {}] found.", deviceIdHash, deviceDiscoveryId); thingDiscovered(result); diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudDeviceHandler.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudDeviceHandler.java index aa51c597ca5e8..30e9aa8f9344f 100644 --- a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudDeviceHandler.java +++ b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/ICloudDeviceHandler.java @@ -185,8 +185,11 @@ private void updateLocationRelatedStates(ICloudDeviceInformation deviceInformati this.logger.debug("Device: [{}]", this.deviceId); for (ICloudDeviceInformation deviceInformationRecord : deviceInformationList) { - String currentId = deviceInformationRecord.getId(); - + String currentId = deviceInformationRecord.getDeviceDiscoveryId(); + if (currentId == null || currentId.isBlank()) { + logger.debug("deviceDiscoveryId is empty, using device name for identification."); + currentId = deviceInformationRecord.getName(); + } logger.debug("Current data element: [id = {}]", currentId); if (currentId != null && currentId.equals(deviceId)) { diff --git a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java index b3a6b8a472ae0..cdbd565342e9e 100644 --- a/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java +++ b/bundles/org.openhab.binding.icloud/src/main/java/org/openhab/binding/icloud/internal/handler/dto/json/response/ICloudDeviceInformation.java @@ -50,6 +50,8 @@ public class ICloudDeviceInformation { private String id; + private String deviceDiscoveryId; + private boolean isLocating; private boolean isMac; @@ -160,6 +162,10 @@ public String getId() { return this.id; } + public String getDeviceDiscoveryId() { + return this.deviceDiscoveryId; + } + public boolean getIsLocating() { return this.isLocating; } diff --git a/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java b/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java index 126fd8eac46fc..da9af630ca260 100644 --- a/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java +++ b/bundles/org.openhab.binding.icloud/src/test/java/org/openhab/binding/icloud/TestICloud.java @@ -13,22 +13,46 @@ package org.openhab.binding.icloud; import static org.junit.jupiter.api.Assertions.*; +import static org.openhab.binding.icloud.internal.ICloudBindingConstants.THING_TYPE_ICLOUDDEVICE; import java.io.File; import java.io.IOException; import java.io.PrintStream; +import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Scanner; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; 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.ICloudBindingConstants; import org.openhab.binding.icloud.internal.ICloudService; +import org.openhab.binding.icloud.internal.handler.ICloudDeviceHandler; import org.openhab.binding.icloud.internal.handler.dto.json.response.ICloudAccountDataResponse; import org.openhab.binding.icloud.internal.utilities.JsonUtils; +import org.openhab.core.config.core.ConfigDescription; +import org.openhab.core.config.core.Configuration; import org.openhab.core.storage.json.internal.JsonStorage; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelGroupUID; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusInfo; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandlerCallback; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.internal.ThingImpl; +import org.openhab.core.thing.type.ChannelGroupTypeUID; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,4 +118,403 @@ public void testAuth() throws IOException, InterruptedException, ICloudApiRespon assertNotNull(deviceInfo); stateStorage.flush(); } + + @Test + @EnabledIfSystemProperty(named = "icloud.test.email", matches = ".*", disabledReason = "Only for manual execution.") + public void testDiscovery() { + + String icloudDeviceRespond = """ + { + "userInfo": { + "accountFormatter": 0, + "firstName": "Firstname", + "lastName": "Lastname", + "membersInfo": { + "XXX~": { + "accountFormatter": 0, + "firstName": "Firstname", + "lastName": "Lastname", + "deviceFetchStatus": "LOADING", + "useAuthWidget": true, + "isHSA": true, + "appleId": "dummy@dummy.local" + } + }, + "hasMembers": true + }, + "alert": null, + "serverContext": { + "minCallbackIntervalInMS": 5000, + "preferredLanguage": "de-de", + "enable2FAFamilyActions": false, + "lastSessionExtensionTime": null, + "validRegion": true, + "callbackIntervalInMS": 2000, + "enableMapStats": true, + "timezone": { + "currentOffset": -25200000, + "previousTransition": 1678615199999, + "previousOffset": -28800000, + "tzCurrentName": "Pacific Daylight Time", + "tzName": "America/Los_Angeles" + }, + "authToken": null, + "maxCallbackIntervalInMS": 60000, + "classicUser": false, + "isHSA": true, + "trackInfoCacheDurationInSecs": 86400, + "imageBaseUrl": "https://statici.icloud.com", + "minTrackLocThresholdInMts": 100, + "itemLearnMoreURL": "https://support.apple.com/kb/HT211331?viewlocale=de_DE", + "maxLocatingTime": 90000, + "itemsTabEnabled": true, + "sessionLifespan": 900000, + "info": "", + "prefsUpdateTime": 1679378160178, + "useAuthWidget": true, + "inaccuracyRadiusThreshold": 200, + "clientId": "", + "serverTimestamp": 1679640300550, + "enable2FAFamilyRemove": false, + "deviceImageVersion": "22", + "macCount": 0, + "deviceLoadStatus": "200", + "maxDeviceLoadTime": 60000, + "prsId": 146786264, + "showSllNow": false, + "cloudUser": true, + "enable2FAErase": false + }, + "userPreferences": { + "webPrefs": { + "id": "web_prefs", + "selectedDeviceId": "XXX" + } + }, + "content": [ + { + "msg": null, + "activationLocked": true, + "passcodeLength": 6, + "features": { + "BTR": false, + "LLC": false, + "CLK": false, + "TEU": true, + "SND": true, + "ALS": false, + "CLT": false, + "SVP": false, + "SPN": false, + "XRM": false, + "NWF": true, + "CWP": false, + "MSG": true, + "LOC": true, + "LME": false, + "LMG": false, + "LYU": false, + "LKL": false, + "LST": true, + "LKM": false, + "WMG": true, + "SCA": false, + "PSS": false, + "EAL": false, + "LAE": false, + "PIN": false, + "LCK": true, + "REM": true, + "MCS": false, + "KEY": false, + "KPD": false, + "WIP": true + }, + "scd": false, + "id": "device.id", + "remoteLock": null, + "rm2State": 0, + "modelDisplayName": "iPad", + "fmlyShare": false, + "lostModeCapable": true, + "wipedTimestamp": null, + "encodedDeviceId": null, + "scdPh": "", + "locationCapable": true, + "trackingInfo": null, + "name": "My iPad", + "isMac": false, + "thisDevice": false, + "deviceClass": "iPad", + "nwd": false, + "remoteWipe": null, + "canWipeAfterLock": true, + "baUUID": "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + "lostModeEnabled": false, + "wipeInProgress": false, + "deviceStatus": "200", + "deviceColor": "1-1-0", + "isConsideredAccessory": false, + "deviceWithYou": false, + "lowPowerMode": false, + "rawDeviceModel": "iPad13,1", + "deviceDiscoveryId": "AAAAAAAA-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + "isLocating": true, + "lostTimestamp": "", + "mesg": null, + "batteryLevel": 0.1599999964237213, + "locationEnabled": true, + "lockedTimestamp": null, + "locFoundEnabled": false, + "snd": null, + "lostDevice": null, + "deviceDisplayName": "iPad Air (4th Generation)", + "prsId": null, + "audioChannels": [ + + ], + "batteryStatus": "NotCharging", + "location": { + "isOld": false, + "isInaccurate": false, + "altitude": 0.0, + "positionType": "Unknown", + "secureLocation": null, + "secureLocationTs": 0, + "latitude": 20.00000000000000, + "floorLevel": 0, + "horizontalAccuracy": 1.0, + "locationType": "", + "timeStamp": 1679640074735, + "locationFinished": true, + "verticalAccuracy": 0.0, + "locationMode": null, + "longitude": 10.00000000000000 + }, + "deviceModel": "FourthGen-1-1-0", + "maxMsgChar": 160, + "darkWake": false + }, + { + "msg": null, + "activationLocked": true, + "passcodeLength": 6, + "features": { + "BTR": false, + "LLC": false, + "CLK": false, + "TEU": true, + "SND": true, + "ALS": false, + "CLT": false, + "SVP": false, + "SPN": false, + "XRM": false, + "NWF": true, + "CWP": false, + "MSG": true, + "LOC": true, + "LME": false, + "LMG": false, + "LYU": false, + "LKL": false, + "LST": true, + "LKM": false, + "WMG": true, + "SCA": false, + "PSS": false, + "EAL": false, + "LAE": false, + "PIN": false, + "LCK": true, + "REM": true, + "MCS": false, + "KEY": false, + "KPD": false, + "WIP": true + }, + "scd": false, + "id": "device.id", + "remoteLock": null, + "rm2State": 0, + "modelDisplayName": "iPad", + "fmlyShare": false, + "lostModeCapable": true, + "wipedTimestamp": null, + "encodedDeviceId": null, + "scdPh": "", + "locationCapable": true, + "trackingInfo": null, + "name": "iPad Air without ID", + "isMac": false, + "thisDevice": false, + "deviceClass": "iPad", + "nwd": false, + "remoteWipe": null, + "canWipeAfterLock": true, + "baUUID": "", + "lostModeEnabled": false, + "wipeInProgress": false, + "deviceStatus": "200", + "deviceColor": "1-1-0", + "isConsideredAccessory": false, + "deviceWithYou": false, + "lowPowerMode": false, + "rawDeviceModel": "iPad13,1", + "deviceDiscoveryId": "", + "isLocating": true, + "lostTimestamp": "", + "mesg": null, + "batteryLevel": 0.1599999964237213, + "locationEnabled": true, + "lockedTimestamp": null, + "locFoundEnabled": false, + "snd": null, + "lostDevice": null, + "deviceDisplayName": "iPad", + "prsId": null, + "audioChannels": [ + + ], + "batteryStatus": "NotCharging", + "location": { + "isOld": false, + "isInaccurate": false, + "altitude": 0.0, + "positionType": "Unknown", + "secureLocation": null, + "secureLocationTs": 0, + "latitude": 20.00000000000000, + "floorLevel": 0, + "horizontalAccuracy": 1.0, + "locationType": "", + "timeStamp": 1679640074735, + "locationFinished": true, + "verticalAccuracy": 0.0, + "locationMode": null, + "longitude": 10.00000000000000 + }, + "deviceModel": "FourthGen-1-1-0", + "maxMsgChar": 160, + "darkWake": false + } + ], + "statusCode": "200" + } + """; + ICloudAccountDataResponse deviceInfo = JsonUtils.fromJson(icloudDeviceRespond, ICloudAccountDataResponse.class); + + // Check device with discoveryId + ThingImpl thing = createThing("AAAAAAAA-FFFF-FFFF-FFFF-FFFFFFFFFFFF"); + ICloudDeviceHandler handler = createDeviceHandler(thing); + + handler.deviceInformationUpdate(deviceInfo.getICloudDeviceInformationList()); + + assertEquals(ThingStatus.ONLINE, handler.getThing().getStatus()); + + // Check device without discoveryId + thing = createThing("iPad Air without ID"); + handler = createDeviceHandler(thing); + + handler.deviceInformationUpdate(deviceInfo.getICloudDeviceInformationList()); + + assertEquals(ThingStatus.ONLINE, handler.getThing().getStatus()); + } + + /** + * @return + */ + private ThingImpl createThing(String deviceId) { + String deviceIdHash = Integer.toHexString(deviceId.hashCode()); + + ThingUID uid = new ThingUID(THING_TYPE_ICLOUDDEVICE, "dummyBridgeId", deviceIdHash); + ThingImpl thing = new ThingImpl(THING_TYPE_ICLOUDDEVICE, uid); + thing.getConfiguration().put(ICloudBindingConstants.DEVICE_PROPERTY_ID, deviceId); + return thing; + } + + /** + * @param thing + * @return + */ + private ICloudDeviceHandler createDeviceHandler(ThingImpl thing) { + ICloudDeviceHandler handler = new ICloudDeviceHandler(thing); + handler.setCallback(new ThingHandlerCallback() { + + @Override + public void validateConfigurationParameters(Channel channel, Map configurationParameters) { + } + + @Override + public void validateConfigurationParameters(Thing thing, Map configurationParameters) { + } + + @Override + public void thingUpdated(Thing thing) { + } + + @Override + public void statusUpdated(Thing thing, ThingStatusInfo thingStatus) { + thing.setStatusInfo(thingStatus); + } + + @Override + public void stateUpdated(ChannelUID channelUID, State state) { + } + + @Override + public void postCommand(ChannelUID channelUID, Command command) { + } + + @Override + public void migrateThingType(Thing thing, ThingTypeUID thingTypeUID, Configuration configuration) { + } + + @Override + public boolean isChannelLinked(ChannelUID channelUID) { + return false; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ThingTypeUID thingTypeUID) { + return null; + } + + @Override + public @Nullable ConfigDescription getConfigDescription(ChannelTypeUID channelTypeUID) { + return null; + } + + @Override + public @Nullable Bridge getBridge(ThingUID bridgeUID) { + return null; + } + + @Override + public ChannelBuilder editChannel(Thing thing, ChannelUID channelUID) { + return ChannelBuilder.create(channelUID); // dummy implementation, probably won't work. + } + + @Override + public List createChannelBuilders(ChannelGroupUID channelGroupUID, + ChannelGroupTypeUID channelGroupTypeUID) { + return new ArrayList(); // dummy implementation, probably won't work. + } + + @Override + public ChannelBuilder createChannelBuilder(ChannelUID channelUID, ChannelTypeUID channelTypeUID) { + return ChannelBuilder.create(channelUID); // dummy implementation, probably won't work. + } + + @Override + public void configurationUpdated(Thing thing) { + } + + @Override + public void channelTriggered(Thing thing, ChannelUID channelUID, String event) { + } + }); + handler.initialize(); + return handler; + } }