├── .github └── workflows │ └── objective-c-xcode.yml ├── FindMyDevices.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── FindMyDevices.xcscheme ├── FindMyDevices.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── swiftpm │ └── Package.resolved ├── FindMyDevices ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── appstore1024.png │ │ ├── ipad152.png │ │ ├── ipad76.png │ │ ├── ipadNotification20.png │ │ ├── ipadNotification40.png │ │ ├── ipadPro167.png │ │ ├── ipadSettings29.png │ │ ├── ipadSettings58.png │ │ ├── ipadSpotlight40.png │ │ ├── ipadSpotlight80.png │ │ ├── iphone120.png │ │ ├── iphone180.png │ │ ├── mac1024.png │ │ ├── mac128.png │ │ ├── mac16.png │ │ ├── mac256.png │ │ ├── mac32.png │ │ ├── mac512.png │ │ ├── mac64.png │ │ ├── notification40.png │ │ ├── notification60.png │ │ ├── settings58.png │ │ ├── settings87.png │ │ ├── spotlight120.png │ │ └── spotlight80.png │ ├── Contents.json │ ├── HALogo.imageset │ │ ├── Contents.json │ │ └── HALogo.png │ └── MQTTLogo.imageset │ │ ├── Contents.json │ │ └── mqtt-icon-transparent.png ├── ContentView.swift ├── Device.swift ├── DeviceDetails.swift ├── DevicesManager.swift ├── DirectoryMonitor.swift ├── Extensions.swift ├── FindMyDevices.entitlements ├── FindMyDevicesApp.swift ├── GeneralSettingsView.swift ├── HomeAssistantSettingsView.swift ├── Info.plist ├── MQTTSettingsView.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json └── SettingsView.swift ├── FindMyDevicesTests └── FindMyDevicesTests.swift ├── FindMyDevicesUITests ├── FindMyDevicesUITests.swift └── FindMyDevicesUITestsLaunchTests.swift ├── LICENSE └── README.md /.github/workflows/objective-c-xcode.yml: -------------------------------------------------------------------------------- 1 | name: Xcode - Build and Analyze 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | name: Build and analyse default scheme using xcodebuild command 12 | runs-on: macos-l4 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Set Default Scheme 17 | run: | 18 | scheme_list=$(xcodebuild -list -json | tr -d "\n") 19 | default=$(echo $scheme_list | ruby -e "require 'json'; puts JSON.parse(STDIN.gets)['project']['targets'][0]") 20 | echo $default | cat >default 21 | echo Using default scheme: $default 22 | - name: Build 23 | env: 24 | scheme: ${{ 'default' }} 25 | run: | 26 | if [ $scheme = default ]; then scheme=$(cat default); fi 27 | if [ "`ls -A | grep -i \\.xcworkspace\$`" ]; then filetype_parameter="workspace" && file_to_build="`ls -A | grep -i \\.xcworkspace\$`"; else filetype_parameter="project" && file_to_build="`ls -A | grep -i \\.xcodeproj\$`"; fi 28 | file_to_build=`echo $file_to_build | awk '{$1=$1;print}'` 29 | xcodebuild clean build analyze -scheme "$scheme" -"$filetype_parameter" "$file_to_build" | xcpretty && exit ${PIPESTATUS[0]} 30 | -------------------------------------------------------------------------------- /FindMyDevices.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4902865F2B76691000FF191E /* DeviceDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4902865E2B76691000FF191E /* DeviceDetails.swift */; }; 11 | 490532CD2B73CF6400C8FE09 /* FindMyDevicesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532CC2B73CF6400C8FE09 /* FindMyDevicesApp.swift */; }; 12 | 490532CF2B73CF6400C8FE09 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532CE2B73CF6400C8FE09 /* ContentView.swift */; }; 13 | 490532D12B73CF6500C8FE09 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 490532D02B73CF6500C8FE09 /* Assets.xcassets */; }; 14 | 490532D42B73CF6500C8FE09 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 490532D32B73CF6500C8FE09 /* Preview Assets.xcassets */; }; 15 | 490532DF2B73CF6500C8FE09 /* FindMyDevicesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532DE2B73CF6500C8FE09 /* FindMyDevicesTests.swift */; }; 16 | 490532E92B73CF6500C8FE09 /* FindMyDevicesUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532E82B73CF6500C8FE09 /* FindMyDevicesUITests.swift */; }; 17 | 490532EB2B73CF6500C8FE09 /* FindMyDevicesUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532EA2B73CF6500C8FE09 /* FindMyDevicesUITestsLaunchTests.swift */; }; 18 | 490532F82B73CF8400C8FE09 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532F72B73CF8400C8FE09 /* Device.swift */; }; 19 | 490532FA2B73D62300C8FE09 /* DevicesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490532F92B73D62300C8FE09 /* DevicesManager.swift */; }; 20 | 494912062B7CD21B00A9F138 /* MQTTNIO in Frameworks */ = {isa = PBXBuildFile; productRef = 494912052B7CD21B00A9F138 /* MQTTNIO */; }; 21 | 49C8056C2B78E6F300552267 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C8056B2B78E6F300552267 /* SettingsView.swift */; }; 22 | 49C8056E2B78E72C00552267 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C8056D2B78E72C00552267 /* GeneralSettingsView.swift */; }; 23 | 49C805702B78E74D00552267 /* HomeAssistantSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49C8056F2B78E74D00552267 /* HomeAssistantSettingsView.swift */; }; 24 | 49E558AE2B7CD9C100C0C70B /* MQTTSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49E558AD2B7CD9C100C0C70B /* MQTTSettingsView.swift */; }; 25 | 49FCF16B2B74F9EB00B78D76 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FCF16A2B74F9EB00B78D76 /* Extensions.swift */; }; 26 | 49FCF16D2B7514DE00B78D76 /* DirectoryMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FCF16C2B7514DE00B78D76 /* DirectoryMonitor.swift */; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXContainerItemProxy section */ 30 | 490532DB2B73CF6500C8FE09 /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = 490532C12B73CF6400C8FE09 /* Project object */; 33 | proxyType = 1; 34 | remoteGlobalIDString = 490532C82B73CF6400C8FE09; 35 | remoteInfo = FindMyDevices; 36 | }; 37 | 490532E52B73CF6500C8FE09 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = 490532C12B73CF6400C8FE09 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = 490532C82B73CF6400C8FE09; 42 | remoteInfo = FindMyDevices; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | 4902865E2B76691000FF191E /* DeviceDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceDetails.swift; sourceTree = ""; }; 48 | 490532C92B73CF6400C8FE09 /* FindMyDevices.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FindMyDevices.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 490532CC2B73CF6400C8FE09 /* FindMyDevicesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindMyDevicesApp.swift; sourceTree = ""; }; 50 | 490532CE2B73CF6400C8FE09 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 51 | 490532D02B73CF6500C8FE09 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | 490532D32B73CF6500C8FE09 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 53 | 490532DA2B73CF6500C8FE09 /* FindMyDevicesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FindMyDevicesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 54 | 490532DE2B73CF6500C8FE09 /* FindMyDevicesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindMyDevicesTests.swift; sourceTree = ""; }; 55 | 490532E42B73CF6500C8FE09 /* FindMyDevicesUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FindMyDevicesUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | 490532E82B73CF6500C8FE09 /* FindMyDevicesUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindMyDevicesUITests.swift; sourceTree = ""; }; 57 | 490532EA2B73CF6500C8FE09 /* FindMyDevicesUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindMyDevicesUITestsLaunchTests.swift; sourceTree = ""; }; 58 | 490532F72B73CF8400C8FE09 /* Device.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Device.swift; sourceTree = ""; }; 59 | 490532F92B73D62300C8FE09 /* DevicesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesManager.swift; sourceTree = ""; }; 60 | 49C805672B77E02C00552267 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 61 | 49C8056B2B78E6F300552267 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 62 | 49C8056D2B78E72C00552267 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; 63 | 49C8056F2B78E74D00552267 /* HomeAssistantSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAssistantSettingsView.swift; sourceTree = ""; }; 64 | 49E558AD2B7CD9C100C0C70B /* MQTTSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTSettingsView.swift; sourceTree = ""; }; 65 | 49FCF16A2B74F9EB00B78D76 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; }; 66 | 49FCF16C2B7514DE00B78D76 /* DirectoryMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryMonitor.swift; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 490532C62B73CF6400C8FE09 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | 494912062B7CD21B00A9F138 /* MQTTNIO in Frameworks */, 75 | ); 76 | runOnlyForDeploymentPostprocessing = 0; 77 | }; 78 | 490532D72B73CF6500C8FE09 /* Frameworks */ = { 79 | isa = PBXFrameworksBuildPhase; 80 | buildActionMask = 2147483647; 81 | files = ( 82 | ); 83 | runOnlyForDeploymentPostprocessing = 0; 84 | }; 85 | 490532E12B73CF6500C8FE09 /* Frameworks */ = { 86 | isa = PBXFrameworksBuildPhase; 87 | buildActionMask = 2147483647; 88 | files = ( 89 | ); 90 | runOnlyForDeploymentPostprocessing = 0; 91 | }; 92 | /* End PBXFrameworksBuildPhase section */ 93 | 94 | /* Begin PBXGroup section */ 95 | 490532C02B73CF6400C8FE09 = { 96 | isa = PBXGroup; 97 | children = ( 98 | 490532CB2B73CF6400C8FE09 /* FindMyDevices */, 99 | 490532DD2B73CF6500C8FE09 /* FindMyDevicesTests */, 100 | 490532E72B73CF6500C8FE09 /* FindMyDevicesUITests */, 101 | 490532CA2B73CF6400C8FE09 /* Products */, 102 | ); 103 | sourceTree = ""; 104 | }; 105 | 490532CA2B73CF6400C8FE09 /* Products */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 490532C92B73CF6400C8FE09 /* FindMyDevices.app */, 109 | 490532DA2B73CF6500C8FE09 /* FindMyDevicesTests.xctest */, 110 | 490532E42B73CF6500C8FE09 /* FindMyDevicesUITests.xctest */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | 490532CB2B73CF6400C8FE09 /* FindMyDevices */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 49C805672B77E02C00552267 /* Info.plist */, 119 | 490532CC2B73CF6400C8FE09 /* FindMyDevicesApp.swift */, 120 | 490532CE2B73CF6400C8FE09 /* ContentView.swift */, 121 | 490532F72B73CF8400C8FE09 /* Device.swift */, 122 | 490532F92B73D62300C8FE09 /* DevicesManager.swift */, 123 | 49FCF16A2B74F9EB00B78D76 /* Extensions.swift */, 124 | 490532D02B73CF6500C8FE09 /* Assets.xcassets */, 125 | 490532D22B73CF6500C8FE09 /* Preview Content */, 126 | 49FCF16C2B7514DE00B78D76 /* DirectoryMonitor.swift */, 127 | 4902865E2B76691000FF191E /* DeviceDetails.swift */, 128 | 49C8056B2B78E6F300552267 /* SettingsView.swift */, 129 | 49C8056D2B78E72C00552267 /* GeneralSettingsView.swift */, 130 | 49C8056F2B78E74D00552267 /* HomeAssistantSettingsView.swift */, 131 | 49E558AD2B7CD9C100C0C70B /* MQTTSettingsView.swift */, 132 | ); 133 | path = FindMyDevices; 134 | sourceTree = ""; 135 | }; 136 | 490532D22B73CF6500C8FE09 /* Preview Content */ = { 137 | isa = PBXGroup; 138 | children = ( 139 | 490532D32B73CF6500C8FE09 /* Preview Assets.xcassets */, 140 | ); 141 | path = "Preview Content"; 142 | sourceTree = ""; 143 | }; 144 | 490532DD2B73CF6500C8FE09 /* FindMyDevicesTests */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 490532DE2B73CF6500C8FE09 /* FindMyDevicesTests.swift */, 148 | ); 149 | path = FindMyDevicesTests; 150 | sourceTree = ""; 151 | }; 152 | 490532E72B73CF6500C8FE09 /* FindMyDevicesUITests */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 490532E82B73CF6500C8FE09 /* FindMyDevicesUITests.swift */, 156 | 490532EA2B73CF6500C8FE09 /* FindMyDevicesUITestsLaunchTests.swift */, 157 | ); 158 | path = FindMyDevicesUITests; 159 | sourceTree = ""; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | 490532C82B73CF6400C8FE09 /* FindMyDevices */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = 490532EE2B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevices" */; 167 | buildPhases = ( 168 | 490532C52B73CF6400C8FE09 /* Sources */, 169 | 490532C62B73CF6400C8FE09 /* Frameworks */, 170 | 490532C72B73CF6400C8FE09 /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = FindMyDevices; 177 | packageProductDependencies = ( 178 | 494912052B7CD21B00A9F138 /* MQTTNIO */, 179 | ); 180 | productName = FindMyDevices; 181 | productReference = 490532C92B73CF6400C8FE09 /* FindMyDevices.app */; 182 | productType = "com.apple.product-type.application"; 183 | }; 184 | 490532D92B73CF6500C8FE09 /* FindMyDevicesTests */ = { 185 | isa = PBXNativeTarget; 186 | buildConfigurationList = 490532F12B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevicesTests" */; 187 | buildPhases = ( 188 | 490532D62B73CF6500C8FE09 /* Sources */, 189 | 490532D72B73CF6500C8FE09 /* Frameworks */, 190 | 490532D82B73CF6500C8FE09 /* Resources */, 191 | ); 192 | buildRules = ( 193 | ); 194 | dependencies = ( 195 | 490532DC2B73CF6500C8FE09 /* PBXTargetDependency */, 196 | ); 197 | name = FindMyDevicesTests; 198 | productName = FindMyDevicesTests; 199 | productReference = 490532DA2B73CF6500C8FE09 /* FindMyDevicesTests.xctest */; 200 | productType = "com.apple.product-type.bundle.unit-test"; 201 | }; 202 | 490532E32B73CF6500C8FE09 /* FindMyDevicesUITests */ = { 203 | isa = PBXNativeTarget; 204 | buildConfigurationList = 490532F42B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevicesUITests" */; 205 | buildPhases = ( 206 | 490532E02B73CF6500C8FE09 /* Sources */, 207 | 490532E12B73CF6500C8FE09 /* Frameworks */, 208 | 490532E22B73CF6500C8FE09 /* Resources */, 209 | ); 210 | buildRules = ( 211 | ); 212 | dependencies = ( 213 | 490532E62B73CF6500C8FE09 /* PBXTargetDependency */, 214 | ); 215 | name = FindMyDevicesUITests; 216 | productName = FindMyDevicesUITests; 217 | productReference = 490532E42B73CF6500C8FE09 /* FindMyDevicesUITests.xctest */; 218 | productType = "com.apple.product-type.bundle.ui-testing"; 219 | }; 220 | /* End PBXNativeTarget section */ 221 | 222 | /* Begin PBXProject section */ 223 | 490532C12B73CF6400C8FE09 /* Project object */ = { 224 | isa = PBXProject; 225 | attributes = { 226 | BuildIndependentTargetsInParallel = 1; 227 | LastSwiftUpdateCheck = 1530; 228 | LastUpgradeCheck = 1530; 229 | TargetAttributes = { 230 | 490532C82B73CF6400C8FE09 = { 231 | CreatedOnToolsVersion = 15.3; 232 | }; 233 | 490532D92B73CF6500C8FE09 = { 234 | CreatedOnToolsVersion = 15.3; 235 | TestTargetID = 490532C82B73CF6400C8FE09; 236 | }; 237 | 490532E32B73CF6500C8FE09 = { 238 | CreatedOnToolsVersion = 15.3; 239 | TestTargetID = 490532C82B73CF6400C8FE09; 240 | }; 241 | }; 242 | }; 243 | buildConfigurationList = 490532C42B73CF6400C8FE09 /* Build configuration list for PBXProject "FindMyDevices" */; 244 | compatibilityVersion = "Xcode 14.0"; 245 | developmentRegion = en; 246 | hasScannedForEncodings = 0; 247 | knownRegions = ( 248 | en, 249 | Base, 250 | ); 251 | mainGroup = 490532C02B73CF6400C8FE09; 252 | packageReferences = ( 253 | 494912042B7CD21B00A9F138 /* XCRemoteSwiftPackageReference "mqtt-nio" */, 254 | ); 255 | productRefGroup = 490532CA2B73CF6400C8FE09 /* Products */; 256 | projectDirPath = ""; 257 | projectRoot = ""; 258 | targets = ( 259 | 490532C82B73CF6400C8FE09 /* FindMyDevices */, 260 | 490532D92B73CF6500C8FE09 /* FindMyDevicesTests */, 261 | 490532E32B73CF6500C8FE09 /* FindMyDevicesUITests */, 262 | ); 263 | }; 264 | /* End PBXProject section */ 265 | 266 | /* Begin PBXResourcesBuildPhase section */ 267 | 490532C72B73CF6400C8FE09 /* Resources */ = { 268 | isa = PBXResourcesBuildPhase; 269 | buildActionMask = 2147483647; 270 | files = ( 271 | 490532D42B73CF6500C8FE09 /* Preview Assets.xcassets in Resources */, 272 | 490532D12B73CF6500C8FE09 /* Assets.xcassets in Resources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | 490532D82B73CF6500C8FE09 /* Resources */ = { 277 | isa = PBXResourcesBuildPhase; 278 | buildActionMask = 2147483647; 279 | files = ( 280 | ); 281 | runOnlyForDeploymentPostprocessing = 0; 282 | }; 283 | 490532E22B73CF6500C8FE09 /* Resources */ = { 284 | isa = PBXResourcesBuildPhase; 285 | buildActionMask = 2147483647; 286 | files = ( 287 | ); 288 | runOnlyForDeploymentPostprocessing = 0; 289 | }; 290 | /* End PBXResourcesBuildPhase section */ 291 | 292 | /* Begin PBXSourcesBuildPhase section */ 293 | 490532C52B73CF6400C8FE09 /* Sources */ = { 294 | isa = PBXSourcesBuildPhase; 295 | buildActionMask = 2147483647; 296 | files = ( 297 | 49FCF16B2B74F9EB00B78D76 /* Extensions.swift in Sources */, 298 | 49C8056C2B78E6F300552267 /* SettingsView.swift in Sources */, 299 | 49C8056E2B78E72C00552267 /* GeneralSettingsView.swift in Sources */, 300 | 490532F82B73CF8400C8FE09 /* Device.swift in Sources */, 301 | 490532FA2B73D62300C8FE09 /* DevicesManager.swift in Sources */, 302 | 490532CF2B73CF6400C8FE09 /* ContentView.swift in Sources */, 303 | 49FCF16D2B7514DE00B78D76 /* DirectoryMonitor.swift in Sources */, 304 | 49E558AE2B7CD9C100C0C70B /* MQTTSettingsView.swift in Sources */, 305 | 490532CD2B73CF6400C8FE09 /* FindMyDevicesApp.swift in Sources */, 306 | 49C805702B78E74D00552267 /* HomeAssistantSettingsView.swift in Sources */, 307 | 4902865F2B76691000FF191E /* DeviceDetails.swift in Sources */, 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | }; 311 | 490532D62B73CF6500C8FE09 /* Sources */ = { 312 | isa = PBXSourcesBuildPhase; 313 | buildActionMask = 2147483647; 314 | files = ( 315 | 490532DF2B73CF6500C8FE09 /* FindMyDevicesTests.swift in Sources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | 490532E02B73CF6500C8FE09 /* Sources */ = { 320 | isa = PBXSourcesBuildPhase; 321 | buildActionMask = 2147483647; 322 | files = ( 323 | 490532E92B73CF6500C8FE09 /* FindMyDevicesUITests.swift in Sources */, 324 | 490532EB2B73CF6500C8FE09 /* FindMyDevicesUITestsLaunchTests.swift in Sources */, 325 | ); 326 | runOnlyForDeploymentPostprocessing = 0; 327 | }; 328 | /* End PBXSourcesBuildPhase section */ 329 | 330 | /* Begin PBXTargetDependency section */ 331 | 490532DC2B73CF6500C8FE09 /* PBXTargetDependency */ = { 332 | isa = PBXTargetDependency; 333 | target = 490532C82B73CF6400C8FE09 /* FindMyDevices */; 334 | targetProxy = 490532DB2B73CF6500C8FE09 /* PBXContainerItemProxy */; 335 | }; 336 | 490532E62B73CF6500C8FE09 /* PBXTargetDependency */ = { 337 | isa = PBXTargetDependency; 338 | target = 490532C82B73CF6400C8FE09 /* FindMyDevices */; 339 | targetProxy = 490532E52B73CF6500C8FE09 /* PBXContainerItemProxy */; 340 | }; 341 | /* End PBXTargetDependency section */ 342 | 343 | /* Begin XCBuildConfiguration section */ 344 | 490532EC2B73CF6600C8FE09 /* Debug */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 349 | CLANG_ANALYZER_NONNULL = YES; 350 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 351 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 352 | CLANG_ENABLE_MODULES = YES; 353 | CLANG_ENABLE_OBJC_ARC = YES; 354 | CLANG_ENABLE_OBJC_WEAK = YES; 355 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 356 | CLANG_WARN_BOOL_CONVERSION = YES; 357 | CLANG_WARN_COMMA = YES; 358 | CLANG_WARN_CONSTANT_CONVERSION = YES; 359 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 360 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 361 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 362 | CLANG_WARN_EMPTY_BODY = YES; 363 | CLANG_WARN_ENUM_CONVERSION = YES; 364 | CLANG_WARN_INFINITE_RECURSION = YES; 365 | CLANG_WARN_INT_CONVERSION = YES; 366 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 367 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 368 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 371 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 372 | CLANG_WARN_STRICT_PROTOTYPES = YES; 373 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 374 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 375 | CLANG_WARN_UNREACHABLE_CODE = YES; 376 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 377 | COPY_PHASE_STRIP = NO; 378 | DEBUG_INFORMATION_FORMAT = dwarf; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | ENABLE_TESTABILITY = YES; 381 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 382 | GCC_C_LANGUAGE_STANDARD = gnu17; 383 | GCC_DYNAMIC_NO_PIC = NO; 384 | GCC_NO_COMMON_BLOCKS = YES; 385 | GCC_OPTIMIZATION_LEVEL = 0; 386 | GCC_PREPROCESSOR_DEFINITIONS = ( 387 | "DEBUG=1", 388 | "$(inherited)", 389 | ); 390 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 391 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 392 | GCC_WARN_UNDECLARED_SELECTOR = YES; 393 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 394 | GCC_WARN_UNUSED_FUNCTION = YES; 395 | GCC_WARN_UNUSED_VARIABLE = YES; 396 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 397 | MACOSX_DEPLOYMENT_TARGET = 14.4; 398 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 399 | MTL_FAST_MATH = YES; 400 | ONLY_ACTIVE_ARCH = YES; 401 | SDKROOT = macosx; 402 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 403 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 404 | }; 405 | name = Debug; 406 | }; 407 | 490532ED2B73CF6600C8FE09 /* Release */ = { 408 | isa = XCBuildConfiguration; 409 | buildSettings = { 410 | ALWAYS_SEARCH_USER_PATHS = NO; 411 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 412 | CLANG_ANALYZER_NONNULL = YES; 413 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 414 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 415 | CLANG_ENABLE_MODULES = YES; 416 | CLANG_ENABLE_OBJC_ARC = YES; 417 | CLANG_ENABLE_OBJC_WEAK = YES; 418 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 419 | CLANG_WARN_BOOL_CONVERSION = YES; 420 | CLANG_WARN_COMMA = YES; 421 | CLANG_WARN_CONSTANT_CONVERSION = YES; 422 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 423 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 424 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 425 | CLANG_WARN_EMPTY_BODY = YES; 426 | CLANG_WARN_ENUM_CONVERSION = YES; 427 | CLANG_WARN_INFINITE_RECURSION = YES; 428 | CLANG_WARN_INT_CONVERSION = YES; 429 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 430 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 431 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 432 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 433 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 434 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 435 | CLANG_WARN_STRICT_PROTOTYPES = YES; 436 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 437 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 438 | CLANG_WARN_UNREACHABLE_CODE = YES; 439 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 440 | COPY_PHASE_STRIP = NO; 441 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 442 | ENABLE_NS_ASSERTIONS = NO; 443 | ENABLE_STRICT_OBJC_MSGSEND = YES; 444 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 445 | GCC_C_LANGUAGE_STANDARD = gnu17; 446 | GCC_NO_COMMON_BLOCKS = YES; 447 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 448 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 449 | GCC_WARN_UNDECLARED_SELECTOR = YES; 450 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 451 | GCC_WARN_UNUSED_FUNCTION = YES; 452 | GCC_WARN_UNUSED_VARIABLE = YES; 453 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 454 | MACOSX_DEPLOYMENT_TARGET = 14.4; 455 | MTL_ENABLE_DEBUG_INFO = NO; 456 | MTL_FAST_MATH = YES; 457 | SDKROOT = macosx; 458 | SWIFT_COMPILATION_MODE = wholemodule; 459 | }; 460 | name = Release; 461 | }; 462 | 490532EF2B73CF6600C8FE09 /* Debug */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 466 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 467 | CODE_SIGN_ENTITLEMENTS = FindMyDevices/FindMyDevices.entitlements; 468 | CODE_SIGN_IDENTITY = "Apple Development"; 469 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 470 | CODE_SIGN_STYLE = Automatic; 471 | COMBINE_HIDPI_IMAGES = YES; 472 | CURRENT_PROJECT_VERSION = 1; 473 | DEVELOPMENT_ASSET_PATHS = "\"FindMyDevices/Preview Content\""; 474 | ENABLE_HARDENED_RUNTIME = YES; 475 | ENABLE_PREVIEWS = YES; 476 | GENERATE_INFOPLIST_FILE = YES; 477 | INFOPLIST_FILE = FindMyDevices/Info.plist; 478 | INFOPLIST_KEY_CFBundleDisplayName = FindMyDevices; 479 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 480 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 481 | LD_RUNPATH_SEARCH_PATHS = ( 482 | "$(inherited)", 483 | "@executable_path/../Frameworks", 484 | ); 485 | MACOSX_DEPLOYMENT_TARGET = 14.0; 486 | MARKETING_VERSION = 1.0; 487 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevices; 488 | PRODUCT_NAME = "$(TARGET_NAME)"; 489 | PROVISIONING_PROFILE_SPECIFIER = ""; 490 | SWIFT_EMIT_LOC_STRINGS = YES; 491 | SWIFT_VERSION = 5.0; 492 | }; 493 | name = Debug; 494 | }; 495 | 490532F02B73CF6600C8FE09 /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | buildSettings = { 498 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 499 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 500 | CODE_SIGN_ENTITLEMENTS = FindMyDevices/FindMyDevices.entitlements; 501 | CODE_SIGN_IDENTITY = "Apple Development"; 502 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; 503 | CODE_SIGN_STYLE = Automatic; 504 | COMBINE_HIDPI_IMAGES = YES; 505 | CURRENT_PROJECT_VERSION = 1; 506 | DEVELOPMENT_ASSET_PATHS = "\"FindMyDevices/Preview Content\""; 507 | ENABLE_HARDENED_RUNTIME = YES; 508 | ENABLE_PREVIEWS = YES; 509 | GENERATE_INFOPLIST_FILE = YES; 510 | INFOPLIST_FILE = FindMyDevices/Info.plist; 511 | INFOPLIST_KEY_CFBundleDisplayName = FindMyDevices; 512 | INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; 513 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 514 | LD_RUNPATH_SEARCH_PATHS = ( 515 | "$(inherited)", 516 | "@executable_path/../Frameworks", 517 | ); 518 | MACOSX_DEPLOYMENT_TARGET = 14.0; 519 | MARKETING_VERSION = 1.0; 520 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevices; 521 | PRODUCT_NAME = "$(TARGET_NAME)"; 522 | PROVISIONING_PROFILE_SPECIFIER = ""; 523 | SWIFT_EMIT_LOC_STRINGS = YES; 524 | SWIFT_VERSION = 5.0; 525 | }; 526 | name = Release; 527 | }; 528 | 490532F22B73CF6600C8FE09 /* Debug */ = { 529 | isa = XCBuildConfiguration; 530 | buildSettings = { 531 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 532 | BUNDLE_LOADER = "$(TEST_HOST)"; 533 | CODE_SIGN_STYLE = Automatic; 534 | CURRENT_PROJECT_VERSION = 1; 535 | GENERATE_INFOPLIST_FILE = YES; 536 | MACOSX_DEPLOYMENT_TARGET = 14.4; 537 | MARKETING_VERSION = 1.0; 538 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevicesTests; 539 | PRODUCT_NAME = "$(TARGET_NAME)"; 540 | SWIFT_EMIT_LOC_STRINGS = NO; 541 | SWIFT_VERSION = 5.0; 542 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FindMyDevices.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FindMyDevices"; 543 | }; 544 | name = Debug; 545 | }; 546 | 490532F32B73CF6600C8FE09 /* Release */ = { 547 | isa = XCBuildConfiguration; 548 | buildSettings = { 549 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 550 | BUNDLE_LOADER = "$(TEST_HOST)"; 551 | CODE_SIGN_STYLE = Automatic; 552 | CURRENT_PROJECT_VERSION = 1; 553 | GENERATE_INFOPLIST_FILE = YES; 554 | MACOSX_DEPLOYMENT_TARGET = 14.4; 555 | MARKETING_VERSION = 1.0; 556 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevicesTests; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | SWIFT_EMIT_LOC_STRINGS = NO; 559 | SWIFT_VERSION = 5.0; 560 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/FindMyDevices.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/FindMyDevices"; 561 | }; 562 | name = Release; 563 | }; 564 | 490532F52B73CF6600C8FE09 /* Debug */ = { 565 | isa = XCBuildConfiguration; 566 | buildSettings = { 567 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 568 | CODE_SIGN_STYLE = Automatic; 569 | CURRENT_PROJECT_VERSION = 1; 570 | GENERATE_INFOPLIST_FILE = YES; 571 | MARKETING_VERSION = 1.0; 572 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevicesUITests; 573 | PRODUCT_NAME = "$(TARGET_NAME)"; 574 | SWIFT_EMIT_LOC_STRINGS = NO; 575 | SWIFT_VERSION = 5.0; 576 | TEST_TARGET_NAME = FindMyDevices; 577 | }; 578 | name = Debug; 579 | }; 580 | 490532F62B73CF6600C8FE09 /* Release */ = { 581 | isa = XCBuildConfiguration; 582 | buildSettings = { 583 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 584 | CODE_SIGN_STYLE = Automatic; 585 | CURRENT_PROJECT_VERSION = 1; 586 | GENERATE_INFOPLIST_FILE = YES; 587 | MARKETING_VERSION = 1.0; 588 | PRODUCT_BUNDLE_IDENTIFIER = com.airy.FindMyDevicesUITests; 589 | PRODUCT_NAME = "$(TARGET_NAME)"; 590 | SWIFT_EMIT_LOC_STRINGS = NO; 591 | SWIFT_VERSION = 5.0; 592 | TEST_TARGET_NAME = FindMyDevices; 593 | }; 594 | name = Release; 595 | }; 596 | /* End XCBuildConfiguration section */ 597 | 598 | /* Begin XCConfigurationList section */ 599 | 490532C42B73CF6400C8FE09 /* Build configuration list for PBXProject "FindMyDevices" */ = { 600 | isa = XCConfigurationList; 601 | buildConfigurations = ( 602 | 490532EC2B73CF6600C8FE09 /* Debug */, 603 | 490532ED2B73CF6600C8FE09 /* Release */, 604 | ); 605 | defaultConfigurationIsVisible = 0; 606 | defaultConfigurationName = Release; 607 | }; 608 | 490532EE2B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevices" */ = { 609 | isa = XCConfigurationList; 610 | buildConfigurations = ( 611 | 490532EF2B73CF6600C8FE09 /* Debug */, 612 | 490532F02B73CF6600C8FE09 /* Release */, 613 | ); 614 | defaultConfigurationIsVisible = 0; 615 | defaultConfigurationName = Release; 616 | }; 617 | 490532F12B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevicesTests" */ = { 618 | isa = XCConfigurationList; 619 | buildConfigurations = ( 620 | 490532F22B73CF6600C8FE09 /* Debug */, 621 | 490532F32B73CF6600C8FE09 /* Release */, 622 | ); 623 | defaultConfigurationIsVisible = 0; 624 | defaultConfigurationName = Release; 625 | }; 626 | 490532F42B73CF6600C8FE09 /* Build configuration list for PBXNativeTarget "FindMyDevicesUITests" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | 490532F52B73CF6600C8FE09 /* Debug */, 630 | 490532F62B73CF6600C8FE09 /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | /* End XCConfigurationList section */ 636 | 637 | /* Begin XCRemoteSwiftPackageReference section */ 638 | 494912042B7CD21B00A9F138 /* XCRemoteSwiftPackageReference "mqtt-nio" */ = { 639 | isa = XCRemoteSwiftPackageReference; 640 | repositoryURL = "https://github.com/swift-server-community/mqtt-nio"; 641 | requirement = { 642 | kind = upToNextMajorVersion; 643 | minimumVersion = 2.11.0; 644 | }; 645 | }; 646 | /* End XCRemoteSwiftPackageReference section */ 647 | 648 | /* Begin XCSwiftPackageProductDependency section */ 649 | 494912052B7CD21B00A9F138 /* MQTTNIO */ = { 650 | isa = XCSwiftPackageProductDependency; 651 | package = 494912042B7CD21B00A9F138 /* XCRemoteSwiftPackageReference "mqtt-nio" */; 652 | productName = MQTTNIO; 653 | }; 654 | /* End XCSwiftPackageProductDependency section */ 655 | }; 656 | rootObject = 490532C12B73CF6400C8FE09 /* Project object */; 657 | } 658 | -------------------------------------------------------------------------------- /FindMyDevices.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FindMyDevices.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "f4496f27dedf5447f89588eb3ffc19b72ee135d3b3e1d1d7148486cc0b6ef1ad", 3 | "pins" : [ 4 | { 5 | "identity" : "mqtt-nio", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swift-server-community/mqtt-nio", 8 | "state" : { 9 | "revision" : "267b83ab5690d463ff00585a4fd6dc54b698e1d2", 10 | "version" : "2.11.0" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-atomics", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-atomics.git", 17 | "state" : { 18 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 19 | "version" : "1.2.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-collections", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-collections.git", 26 | "state" : { 27 | "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", 28 | "version" : "1.1.0" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-log", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/apple/swift-log.git", 35 | "state" : { 36 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 37 | "version" : "1.5.4" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-nio", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-nio.git", 44 | "state" : { 45 | "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", 46 | "version" : "2.64.0" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-nio-ssl", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-nio-ssl.git", 53 | "state" : { 54 | "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", 55 | "version" : "2.26.0" 56 | } 57 | }, 58 | { 59 | "identity" : "swift-nio-transport-services", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 62 | "state" : { 63 | "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", 64 | "version" : "1.20.1" 65 | } 66 | }, 67 | { 68 | "identity" : "swift-system", 69 | "kind" : "remoteSourceControl", 70 | "location" : "https://github.com/apple/swift-system.git", 71 | "state" : { 72 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 73 | "version" : "1.2.1" 74 | } 75 | } 76 | ], 77 | "version" : 3 78 | } 79 | -------------------------------------------------------------------------------- /FindMyDevices.xcodeproj/xcshareddata/xcschemes/FindMyDevices.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 36 | 42 | 43 | 44 | 47 | 53 | 54 | 55 | 56 | 57 | 67 | 69 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /FindMyDevices.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /FindMyDevices.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "mqtt-nio", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/swift-server-community/mqtt-nio", 7 | "state" : { 8 | "revision" : "267b83ab5690d463ff00585a4fd6dc54b698e1d2", 9 | "version" : "2.11.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-atomics", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/apple/swift-atomics.git", 16 | "state" : { 17 | "revision" : "cd142fd2f64be2100422d658e7411e39489da985", 18 | "version" : "1.2.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-collections", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/apple/swift-collections.git", 25 | "state" : { 26 | "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", 27 | "version" : "1.1.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-log", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-log.git", 34 | "state" : { 35 | "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", 36 | "version" : "1.5.4" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-nio", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/apple/swift-nio.git", 43 | "state" : { 44 | "revision" : "635b2589494c97e48c62514bc8b37ced762e0a62", 45 | "version" : "2.63.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-nio-ssl", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/apple/swift-nio-ssl.git", 52 | "state" : { 53 | "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", 54 | "version" : "2.26.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-nio-transport-services", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-nio-transport-services.git", 61 | "state" : { 62 | "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", 63 | "version" : "1.20.1" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-system", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/apple/swift-system.git", 70 | "state" : { 71 | "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", 72 | "version" : "1.2.1" 73 | } 74 | } 75 | ], 76 | "version" : 2 77 | } 78 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "notification40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "notification60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "settings58.png", 17 | "idiom" : "iphone", 18 | "scale" : "2x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "settings87.png", 23 | "idiom" : "iphone", 24 | "scale" : "3x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "spotlight80.png", 29 | "idiom" : "iphone", 30 | "scale" : "2x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "spotlight120.png", 35 | "idiom" : "iphone", 36 | "scale" : "3x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "iphone120.png", 41 | "idiom" : "iphone", 42 | "scale" : "2x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "filename" : "iphone180.png", 47 | "idiom" : "iphone", 48 | "scale" : "3x", 49 | "size" : "60x60" 50 | }, 51 | { 52 | "filename" : "ipadNotification20.png", 53 | "idiom" : "ipad", 54 | "scale" : "1x", 55 | "size" : "20x20" 56 | }, 57 | { 58 | "filename" : "ipadNotification40.png", 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "20x20" 62 | }, 63 | { 64 | "filename" : "ipadSettings29.png", 65 | "idiom" : "ipad", 66 | "scale" : "1x", 67 | "size" : "29x29" 68 | }, 69 | { 70 | "filename" : "ipadSettings58.png", 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "29x29" 74 | }, 75 | { 76 | "filename" : "ipadSpotlight40.png", 77 | "idiom" : "ipad", 78 | "scale" : "1x", 79 | "size" : "40x40" 80 | }, 81 | { 82 | "filename" : "ipadSpotlight80.png", 83 | "idiom" : "ipad", 84 | "scale" : "2x", 85 | "size" : "40x40" 86 | }, 87 | { 88 | "filename" : "ipad76.png", 89 | "idiom" : "ipad", 90 | "scale" : "1x", 91 | "size" : "76x76" 92 | }, 93 | { 94 | "filename" : "ipad152.png", 95 | "idiom" : "ipad", 96 | "scale" : "2x", 97 | "size" : "76x76" 98 | }, 99 | { 100 | "filename" : "ipadPro167.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "83.5x83.5" 104 | }, 105 | { 106 | "filename" : "appstore1024.png", 107 | "idiom" : "ios-marketing", 108 | "scale" : "1x", 109 | "size" : "1024x1024" 110 | }, 111 | { 112 | "filename" : "mac16.png", 113 | "idiom" : "mac", 114 | "scale" : "1x", 115 | "size" : "16x16" 116 | }, 117 | { 118 | "filename" : "mac32.png", 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "16x16" 122 | }, 123 | { 124 | "filename" : "mac32.png", 125 | "idiom" : "mac", 126 | "scale" : "1x", 127 | "size" : "32x32" 128 | }, 129 | { 130 | "filename" : "mac64.png", 131 | "idiom" : "mac", 132 | "scale" : "2x", 133 | "size" : "32x32" 134 | }, 135 | { 136 | "filename" : "mac128.png", 137 | "idiom" : "mac", 138 | "scale" : "1x", 139 | "size" : "128x128" 140 | }, 141 | { 142 | "filename" : "mac256.png", 143 | "idiom" : "mac", 144 | "scale" : "2x", 145 | "size" : "128x128" 146 | }, 147 | { 148 | "filename" : "mac256.png", 149 | "idiom" : "mac", 150 | "scale" : "1x", 151 | "size" : "256x256" 152 | }, 153 | { 154 | "filename" : "mac512.png", 155 | "idiom" : "mac", 156 | "scale" : "2x", 157 | "size" : "256x256" 158 | }, 159 | { 160 | "filename" : "mac512.png", 161 | "idiom" : "mac", 162 | "scale" : "1x", 163 | "size" : "512x512" 164 | }, 165 | { 166 | "filename" : "mac1024.png", 167 | "idiom" : "mac", 168 | "scale" : "2x", 169 | "size" : "512x512" 170 | } 171 | ], 172 | "info" : { 173 | "author" : "xcode", 174 | "version" : 1 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/appstore1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/appstore1024.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipad152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipad152.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipad76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipad76.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadNotification20.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadNotification40.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadPro167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadPro167.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSettings29.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSettings58.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSpotlight40.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/ipadSpotlight80.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/iphone120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/iphone120.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/iphone180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/iphone180.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac1024.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac128.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac16.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac256.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac32.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac512.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/mac64.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/notification40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/notification40.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/notification60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/notification60.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/settings58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/settings58.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/settings87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/settings87.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/spotlight120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/spotlight120.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/AppIcon.appiconset/spotlight80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/AppIcon.appiconset/spotlight80.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/HALogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "HALogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/HALogo.imageset/HALogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/HALogo.imageset/HALogo.png -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/MQTTLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mqtt-icon-transparent.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /FindMyDevices/Assets.xcassets/MQTTLogo.imageset/mqtt-icon-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airy10/FindMyDevices/cf83a7e662b28ae758f02cefb05abc7800fea005/FindMyDevices/Assets.xcassets/MQTTLogo.imageset/mqtt-icon-transparent.png -------------------------------------------------------------------------------- /FindMyDevices/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | import MapKit 10 | 11 | struct DeviceMarker: MapContent { 12 | @ObservedObject 13 | var device : Device 14 | 15 | let isSelected : Bool 16 | 17 | var body: some MapContent { 18 | 19 | if let latitude = device.latitude, let longitude = device.longitude { 20 | let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 21 | 22 | if let accuracy = device.horizontalAccuracy { 23 | MapCircle(center: location, radius: accuracy) 24 | .mapOverlayLevel(level: isSelected ? .aboveLabels : .aboveRoads) 25 | .foregroundStyle(.teal.opacity(isSelected ? 0.3 : 0.05)) 26 | .stroke(.white, lineWidth: isSelected ? 2.0 : 0.5) 27 | } 28 | 29 | Marker(device.label, coordinate: location) 30 | .tint(isSelected ? .red : .blue) 31 | } 32 | } 33 | } 34 | 35 | struct ContentView: View { 36 | @ObservedObject 37 | var devicesManager : DevicesManager 38 | 39 | @State 40 | private var selection : Device? = nil 41 | 42 | init(devicesManager: DevicesManager = DevicesManager()) { 43 | self.devicesManager = devicesManager 44 | } 45 | 46 | private var selectionIndex: Int? { 47 | return $selection.wrappedValue == nil ? nil : devicesManager.devices.firstIndex(of: $selection.wrappedValue!) 48 | } 49 | 50 | private func device(atIndex index: Int?) -> Device? { 51 | guard let idx = index, (0...devicesManager.devices.count-1) ~= idx else { return nil} 52 | 53 | return devicesManager.devices[idx] 54 | 55 | } 56 | 57 | private func selectPrev() 58 | { 59 | if let selIndex = selectionIndex { 60 | selection = device(atIndex: selIndex - 1) 61 | } else { 62 | selection = devicesManager.devices.last 63 | } 64 | } 65 | 66 | private func selectNext() 67 | { 68 | if let selIndex = selectionIndex { 69 | selection = device(atIndex: selIndex + 1) 70 | } else { 71 | selection = devicesManager.devices.first 72 | } 73 | } 74 | 75 | var body: some View { 76 | HSplitView { 77 | VStack(alignment: .leading) { 78 | 79 | GeometryReader { geometry in 80 | 81 | VStack(alignment: .leading) { 82 | List(devicesManager.devices, selection: $selection) { device in 83 | Text(device.label) 84 | .frame(width: geometry.size.width, alignment: Alignment.leading) 85 | .contentShape(Rectangle()) 86 | .foregroundStyle(device == selection ? AnyShapeStyle(.selection): AnyShapeStyle(.foreground)) 87 | .listRowBackground(device == selection ? Color.accentColor : nil) 88 | .onTapGesture { 89 | selection = device 90 | } 91 | } 92 | .listStyle(PlainListStyle()) 93 | .listItemTint(Color.accentColor) 94 | 95 | .onKeyPress(keys: [ .upArrow, .downArrow]) { 96 | key in 97 | 98 | switch key.key { 99 | case .upArrow: 100 | selectPrev() 101 | case .downArrow: 102 | selectNext() 103 | default: 104 | break 105 | } 106 | 107 | return .handled 108 | } 109 | 110 | // Hack because "onKeyPress" doesn't get down and up arrow keys on macOS 111 | VStack { 112 | Button("") { 113 | selectNext() 114 | } 115 | .keyboardShortcut(KeyboardShortcut(.downArrow, modifiers: [])) 116 | 117 | Button("") { 118 | selectPrev() 119 | } 120 | .keyboardShortcut(KeyboardShortcut(.upArrow, modifiers: [])) 121 | }.hidden() 122 | 123 | if let sel = $selection.wrappedValue { 124 | DeviceDetails(device: sel) 125 | .frame(width: geometry.size.width) 126 | } 127 | } 128 | 129 | } 130 | } 131 | .frame(minWidth: 200) 132 | Map(interactionModes: .all, selection: $selection) { 133 | ForEach(devicesManager.devices, id: \.self) { device in 134 | if let _ = device.latitude, let _ = device.longitude { 135 | 136 | let isSelected = (device == selection) 137 | DeviceMarker(device: device, isSelected: isSelected) 138 | } 139 | } 140 | } 141 | .mapStyle(.hybrid(elevation: .realistic)) 142 | .mapControls { 143 | MapUserLocationButton() 144 | MapCompass() 145 | MapPitchSlider() 146 | MapPitchToggle() 147 | } 148 | .frame(minWidth: 400) 149 | 150 | } 151 | .padding() 152 | } 153 | } 154 | 155 | #Preview { 156 | ContentView() 157 | } 158 | 159 | -------------------------------------------------------------------------------- /FindMyDevices/Device.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Device.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | /// FindMy device descripton 12 | class Device : CustomStringConvertible, 13 | Hashable, Identifiable, ObservableObject { 14 | static func == (lhs: Device, rhs: Device) -> Bool { 15 | return lhs.identifier == rhs.identifier 16 | } 17 | 18 | func hash(into hasher: inout Hasher) { 19 | hasher.combine(identifier) 20 | } 21 | 22 | let identifier : String 23 | 24 | var id : String { 25 | return self.identifier 26 | } 27 | 28 | // From OwnedBeacon 29 | @Published var model: String? 30 | @Published var pairingDate : Date? = nil 31 | 32 | // From BeaconProductInfoRecord 33 | @Published var manufacturerName : String? = nil 34 | @Published var modelName : String? = nil 35 | @Published var version : String? = nil 36 | @Published var iconPath: URL? = nil 37 | 38 | // From BeaconNamingRecord 39 | @Published var name : String? = nil 40 | @Published var emoji : String? = nil 41 | 42 | // From BeaconEstimatedLocation 43 | @Published var horizontalAccuracy: Double? = nil 44 | @Published var longitude: Double? = nil 45 | @Published var latitude: Double? = nil 46 | @Published var timestamp: Date? = nil 47 | @Published var scanDate: Date? = nil 48 | 49 | @Published var battery: Double? = nil 50 | 51 | var label: String { 52 | if emoji != nil { 53 | emoji! + " " + (name ?? (model ?? identifier)) 54 | } else { 55 | name ?? (model ?? identifier) 56 | } 57 | } 58 | 59 | var icon: Image? = nil 60 | 61 | init(identifier: String, model: String?, pairingDate: Date?, manufacturerName: String? = nil, modelName: String? = nil, version: String? = nil, iconPath: URL? = nil, name: String? = nil, emoji: String? = nil, horizontalAccuracy: Double? = nil, longitude: Double? = nil, latitude: Double? = nil, timestamp: Date? = nil, scanDate: Date? = nil, icon: Image? = nil) { 62 | self.identifier = identifier 63 | self.model = model 64 | self.pairingDate = pairingDate 65 | self.manufacturerName = manufacturerName 66 | self.modelName = modelName 67 | self.version = version 68 | self.iconPath = iconPath 69 | self.name = name 70 | self.emoji = emoji 71 | self.horizontalAccuracy = horizontalAccuracy 72 | self.longitude = longitude 73 | self.latitude = latitude 74 | self.timestamp = timestamp 75 | self.scanDate = scanDate 76 | self.icon = icon 77 | } 78 | 79 | var description: String { 80 | return "Device \(identifier) model: \(model ?? "") name: \(name ?? "") emoji: \(emoji ?? "") manufacturerName: \(manufacturerName ?? "") modelName: \(modelName ?? "") latitude: \(latitude ?? 0) longitude: \(longitude ?? 0) timestamp : \(String(describing: timestamp)) scanDate : \(String(describing: scanDate))" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /FindMyDevices/DeviceDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeviceDetails.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 09/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct DeviceDetails: View { 11 | @ObservedObject 12 | var device: Device 13 | 14 | var body: some View { 15 | VStack(alignment: .center) { 16 | HStack { 17 | if let emoji = device.emoji { 18 | Text(emoji).font(.system(.title)) 19 | } 20 | if let name = device.name { 21 | Text(name).font(.system(.title)) 22 | } 23 | } 24 | VStack(alignment: .leading) { 25 | if let manufacturerName = device.manufacturerName { 26 | HStack { 27 | Text("Manufacturer:").frame(width: 150, alignment: .trailing) 28 | Text("\(manufacturerName)") 29 | } 30 | } 31 | if let modelName = device.modelName { 32 | HStack { 33 | Text("Model Name:").frame(width: 150, alignment: .trailing) 34 | Text("\(modelName)") 35 | } 36 | } else if let model = device.model { 37 | HStack { 38 | Text("Model:").frame(width: 150, alignment: .trailing) 39 | Text("\(model)") 40 | } 41 | } 42 | if let time = device.timestamp?.formatted(), let lat = device.latitude, let long = device.longitude { 43 | HStack { 44 | Text("Time:").frame(width: 150, alignment: .trailing) 45 | Text("\(time)") 46 | } 47 | HStack { 48 | Text("Latitude:").frame(width: 150, alignment: .trailing) 49 | Text("\(lat)") 50 | } 51 | HStack { 52 | Text("Longitude:").frame(width: 150, alignment: .trailing) 53 | Text("\(long)") 54 | } 55 | HStack { 56 | Text("Date:").frame(width: 150, alignment: .trailing) 57 | Text("\(time)") 58 | } 59 | } 60 | }.scaledToFill() 61 | Text(device.identifier) 62 | .font(.system(size: 9)) 63 | 64 | } 65 | } 66 | } 67 | 68 | #Preview { 69 | // DeviceDetails(device: $nil) 70 | Button("") {} 71 | } 72 | 73 | -------------------------------------------------------------------------------- /FindMyDevices/DevicesManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DevicesManager.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import Foundation 9 | import CryptoKit 10 | import SwiftUI 11 | import MQTTNIO 12 | import NIOCore 13 | 14 | class DevicesManager : ObservableObject { 15 | 16 | @State 17 | var homeassistantSettings = HomeAssistantSettings() 18 | 19 | @State 20 | var mqttSettings = MQTTSettings() { 21 | didSet { 22 | mqttSettingsDidChange() 23 | } 24 | } 25 | 26 | var disableNotification = true 27 | 28 | var mqttClient : MQTTClient? = nil 29 | 30 | enum Error: Swift.Error { 31 | case invalidFileFormat 32 | case invalidPlistFormat 33 | case invalidDecryptedData 34 | case noPassword 35 | case invalidItem 36 | case keychainError(status: OSStatus) 37 | } 38 | 39 | static private let RootRecordDirURL : URL = { 40 | FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first!.appendingPathComponent("com.apple.icloud.searchpartyd") 41 | }() 42 | static private let OwnedBeaconsDir = "OwnedBeacons" 43 | static private let BeaconProductInfoRecordDir = "BeaconProductInfoRecord" 44 | static private let BeaconEstimatedLocationDir = "BeaconEstimatedLocation" 45 | static private let BeaconNamingRecordDir = "BeaconNamingRecord" 46 | 47 | static private let OwnedBeaconsDirURL : URL = { 48 | RootRecordDirURL.appendingPathComponent(DevicesManager.OwnedBeaconsDir) 49 | }() 50 | static private let BeaconProductInfoRecordDirURL : URL = { 51 | RootRecordDirURL.appendingPathComponent(DevicesManager.BeaconProductInfoRecordDir) 52 | }() 53 | static private let BeaconEstimatedLocationDirURL : URL = { 54 | RootRecordDirURL.appendingPathComponent(DevicesManager.BeaconEstimatedLocationDir) 55 | }() 56 | static private let BeaconNamingRecordDirURL : URL = { 57 | RootRecordDirURL.appendingPathComponent(DevicesManager.BeaconNamingRecordDir) 58 | }() 59 | 60 | private var key : SymmetricKey? = nil 61 | 62 | private var devicesDict : [String : Device] = [:] 63 | 64 | @Published 65 | var devices = [Device]() 66 | 67 | var dirMonitor : DirectoryMonitor? = nil 68 | 69 | init() { 70 | self.loadDevices() 71 | disableNotification = false 72 | 73 | for device in devices { 74 | print("\(device.id) : \(device.label) - \(device.timestamp?.formatted() ?? "")") 75 | } 76 | 77 | Task { 78 | for device in devices { 79 | await notifyChange(device: device) 80 | } 81 | } 82 | } 83 | 84 | deinit { 85 | dirMonitor?.stop() 86 | } 87 | 88 | private func device(id: String) -> Device? 89 | { 90 | return devicesDict[id] 91 | } 92 | 93 | private func loadKey() -> Bool { 94 | if key == nil { 95 | // -> Hex format key from `security find-generic-password -l 'BeaconStore' -w` 96 | let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword, 97 | kSecAttrLabel as String: "BeaconStore", 98 | kSecMatchLimit as String: kSecMatchLimitOne, 99 | kSecReturnAttributes as String: true, 100 | kSecReturnData as String: true] 101 | 102 | var item: CFTypeRef? 103 | let status = SecItemCopyMatching(query as CFDictionary, &item) 104 | if status == errSecSuccess { 105 | if let existingItem = item { 106 | if let keyData = existingItem[kSecValueData as String] as? Data { 107 | key = SymmetricKey(data: keyData) 108 | } 109 | } 110 | } 111 | } 112 | return key != nil 113 | } 114 | 115 | func processOwnedBeacon(_ record: [String: Any]) { 116 | guard let identifier = record["identifier"] as? String else { return } 117 | 118 | let model = record["model"] as? String 119 | let pairingDate = record["pairingDate"] as? Date 120 | 121 | if let device = self.device(id: identifier) { 122 | device.model = model 123 | device.pairingDate = pairingDate 124 | 125 | if !disableNotification { 126 | print("Own beacon changed : \(device)") 127 | } 128 | } else { 129 | self.objectWillChange.send() 130 | 131 | let device = Device(identifier: identifier, model: model, pairingDate: pairingDate) 132 | self.devicesDict[identifier] = device 133 | self.devices.append(device) 134 | } 135 | } 136 | 137 | func processOwnedBeacon(url: URL? = nil) { 138 | let url = url ?? DevicesManager.OwnedBeaconsDirURL 139 | 140 | processRecord(url: url) { 141 | self.processOwnedBeacon($0) 142 | } 143 | } 144 | 145 | func processBeaconProductInfoRecord(_ record: [String: Any]) { 146 | guard let identifier = record["identifier"] as? String else { return } 147 | guard let device = self.device(id: identifier) else { return } 148 | 149 | let manufacturerName = record["manufacturerName"] as? String 150 | let modelName = record["modelName"] as? String 151 | 152 | device.manufacturerName = manufacturerName 153 | device.modelName = modelName 154 | 155 | if !disableNotification { 156 | print("Product info changed : \(device)") 157 | } 158 | 159 | } 160 | 161 | func processBeaconProductInfoRecord(url: URL? = nil) { 162 | let url = url ?? DevicesManager.BeaconProductInfoRecordDirURL 163 | 164 | processRecord(url: url) { 165 | self.processBeaconProductInfoRecord($0) 166 | } 167 | } 168 | 169 | func processBeaconNamingRecord(_ record: [String: Any]) { 170 | guard let identifier = record["associatedBeacon"] as? String else { return } 171 | guard let device = self.device(id: identifier) else { return } 172 | 173 | let name = record["name"] as? String 174 | let emoji = record["emoji"] as? String 175 | 176 | device.name = name 177 | device.emoji = emoji 178 | 179 | if !disableNotification { 180 | print("Naming changed : \(device)") 181 | } 182 | 183 | } 184 | 185 | func processBeaconNamingRecord(url: URL? = nil) { 186 | let url = url ?? DevicesManager.BeaconNamingRecordDirURL 187 | 188 | processRecord(url: url) { 189 | self.processBeaconNamingRecord($0) 190 | } 191 | } 192 | 193 | func processBeaconEstimatedLocation(_ record: [String: Any]) { 194 | guard let identifier = record["associatedBeacon"] as? String else { return } 195 | guard let device = self.device(id: identifier) else { return } 196 | 197 | guard let timestamp = record["timestamp"] as? Date else { return } 198 | 199 | if let currentDeviceTimestamp = device.timestamp { 200 | if timestamp <= currentDeviceTimestamp { 201 | return 202 | } 203 | } 204 | 205 | let latitude = record["latitude"] as? Double 206 | let longitude = record["longitude"] as? Double 207 | let horizontalAccuracy = record["horizontalAccuracy"] as? Double 208 | let scanDate = record["scanDate"] as? Date 209 | 210 | device.latitude = latitude 211 | device.longitude = longitude 212 | device.horizontalAccuracy = horizontalAccuracy 213 | device.scanDate = scanDate 214 | device.timestamp = timestamp 215 | 216 | self.locationChangedFor(device: device) 217 | } 218 | 219 | func locationChangedFor(device: Device) { 220 | if disableNotification { 221 | return 222 | } 223 | 224 | let id = device.identifier.uppercased() 225 | print("Location changed : \(id) : \(device.label) - \(device.timestamp?.formatted() ?? "")") 226 | 227 | Task { 228 | await notifyChange(device: device) 229 | } 230 | } 231 | 232 | func updateMQTT(device: Device) async { 233 | if mqttSettings.enabled == false || mqttSettings.server.count == 0 { 234 | return 235 | } 236 | 237 | let id = device.identifier.uppercased() 238 | 239 | if mqttSettings.server != mqttClient?.host || 240 | mqttSettings.port != mqttClient?.port || 241 | mqttSettings.user != mqttClient?.configuration.userName || 242 | mqttSettings.password != mqttClient?.configuration.password { 243 | 244 | try? mqttClient?.syncShutdownGracefully() 245 | mqttClient = nil 246 | } 247 | 248 | if mqttClient == nil { 249 | mqttClient = MQTTClient( 250 | host: mqttSettings.server, 251 | port: mqttSettings.port, 252 | identifier: "FindMyDevices", 253 | eventLoopGroupProvider: .createNew, 254 | configuration: MQTTClient.Configuration(userName: mqttSettings.user, password: mqttSettings.password) 255 | ) 256 | if let client = mqttClient { 257 | do { 258 | try await client.connect() 259 | print("Connected") 260 | } 261 | catch let error as MQTTError { 262 | print("Error : \(error)") 263 | try? mqttClient?.syncShutdownGracefully() 264 | mqttClient = nil 265 | return 266 | } 267 | catch { 268 | print("Unknown error") 269 | try? mqttClient?.syncShutdownGracefully() 270 | mqttClient = nil 271 | return 272 | } 273 | } else { 274 | print("Invalid client") 275 | } 276 | 277 | } 278 | 279 | let deviceId = "FMD_" + id 280 | let deviceTopic = "homeassistant/device_tracker/" + deviceId + "/" 281 | 282 | let topic = deviceTopic + "config" 283 | 284 | // Create the device (could be done only once - and should be only done if autodiscovery is enabled) 285 | var deviceInfo : [String : Any] = [ 286 | "identifiers": [deviceId], 287 | "name": device.label, 288 | ] 289 | 290 | if let manufacturerName = device.manufacturerName { 291 | deviceInfo["manufacturer"] = manufacturerName 292 | } 293 | if let version = device.version { 294 | deviceInfo["sw_version"] = version 295 | } 296 | if let model = device.model { 297 | deviceInfo["model"] = model 298 | } else if let model = device.modelName { 299 | deviceInfo["model"] = model 300 | } 301 | 302 | let deviceConfig : [String: Any] = [ 303 | "state_topic": deviceTopic + "state", 304 | "json_attributes_topic": deviceTopic + "attributes", 305 | "device": deviceInfo, 306 | "payload_reset" : "reset", 307 | "unique_id": deviceId 308 | ] 309 | if let deviceConfigData = try? JSONSerialization.data(withJSONObject: deviceConfig, options: .withoutEscapingSlashes) { 310 | do { 311 | try await mqttClient?.publish(to: topic, payload: ByteBuffer(data: deviceConfigData), qos: .atLeastOnce, retain: true) 312 | } 313 | catch let error as MQTTError { 314 | print("Error : \(error)") 315 | try? mqttClient?.syncShutdownGracefully() 316 | mqttClient = nil 317 | } 318 | catch { 319 | print("Unknown error") 320 | try? mqttClient?.syncShutdownGracefully() 321 | self.mqttClient = nil 322 | } 323 | } else { 324 | print("Can't encode message : \(deviceConfig)") 325 | } 326 | 327 | var location : [String: Any] = [ 328 | "provider-url": "https://github.com/airy10/FindMyDevices", 329 | "provider": "FindMyDevices", 330 | "via_device": "FindMyDevices", 331 | ] 332 | if let latitude = device.latitude, let longitude = device.longitude { 333 | location["latitude"] = latitude 334 | location["longitude"] = longitude 335 | } 336 | 337 | if let accuracy = device.horizontalAccuracy { 338 | location["gps_accuracy"] = accuracy 339 | } 340 | if let timestamp = device.timestamp { 341 | location["last_update"] = timestamp.ISO8601Format() 342 | location["last_update_timestamp"] = timestamp.timeIntervalSince1970 343 | } 344 | if let battery = device.battery { 345 | location["battery"] = battery 346 | } 347 | 348 | if let locationData = try? JSONSerialization.data(withJSONObject: location, options: .withoutEscapingSlashes) { 349 | do { 350 | try await mqttClient?.publish(to: deviceTopic + "attributes", payload: ByteBuffer(data: locationData), qos: .atLeastOnce, retain: true) 351 | 352 | } 353 | catch let error as MQTTError { 354 | print("Error : \(error)") 355 | try? mqttClient?.syncShutdownGracefully() 356 | mqttClient = nil 357 | } 358 | catch { 359 | print("Unknown error") 360 | try? mqttClient?.syncShutdownGracefully() 361 | mqttClient = nil 362 | } 363 | } 364 | 365 | // _ = mqttClient?.publish(to: deviceTopic + "state", payload: ByteBuffer(string: "reset"), qos: .atLeastOnce, retain: true) 366 | } 367 | 368 | func updateHomeAssistant(device: Device) async { 369 | if homeassistantSettings.enabled == false || homeassistantSettings.endpoint.count == 0 || homeassistantSettings.token.count == 0 { 370 | return 371 | } 372 | 373 | let id = device.identifier.uppercased() 374 | 375 | let sessionConfig = URLSessionConfiguration.default 376 | let session = URLSession( 377 | configuration: sessionConfig, delegate: nil, delegateQueue: nil) 378 | guard let URL = URL(string: homeassistantSettings.endpoint + "/api/services/device_tracker/see") else { return } 379 | var request = URLRequest(url: URL) 380 | request.httpMethod = "POST" 381 | 382 | request.addValue("application/json", forHTTPHeaderField: "Content-Type") 383 | request.addValue("Bearer " + homeassistantSettings.token, forHTTPHeaderField: "Authorization") 384 | 385 | let dev_id = "findmy_" + id.replacingOccurrences(of: "-", with: "") 386 | var attributes: [String: Any] = [ 387 | "dev_id": dev_id, 388 | "mac": "FINDMY_" + id.uppercased(), 389 | "host_name": "FindMyDevices", 390 | ] 391 | if let latitude = device.latitude, let longitude = device.longitude { 392 | attributes["gps"] = [ 393 | latitude, 394 | longitude, 395 | ] 396 | } 397 | if let accuracy = device.horizontalAccuracy { 398 | attributes["gps_accuracy"] = accuracy 399 | } 400 | 401 | if let battery = device.battery { 402 | attributes["battery"] = battery 403 | } 404 | 405 | let jsonData = try! JSONSerialization.data( 406 | withJSONObject: attributes, options: []) 407 | 408 | request.httpBody = jsonData 409 | do { 410 | let (_, response) = try await session.data( 411 | for: request 412 | ) 413 | 414 | if let httpResponse = response as? HTTPURLResponse { 415 | if httpResponse.statusCode != 200 { 416 | print("[" + id + "] Data sent: HTTP \(httpResponse.statusCode)") 417 | } 418 | } 419 | } 420 | catch { 421 | print("[" + id + "] Data send error") 422 | } 423 | } 424 | 425 | func notifyChange(device: Device) async { 426 | 427 | await updateHomeAssistant(device: device) 428 | await updateMQTT(device: device) 429 | 430 | } 431 | 432 | func processBeaconEstimatedLocation(url: URL? = nil) { 433 | let url = url ?? DevicesManager.BeaconEstimatedLocationDirURL 434 | 435 | processRecord(url: url) { 436 | self.processBeaconEstimatedLocation($0) 437 | } 438 | } 439 | 440 | func processRecord(url: URL, code: @escaping ([String: Any]) -> Void) { 441 | guard let decryptKey = self.key else { return } 442 | 443 | if url.isDirectory { 444 | if let urls = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) { 445 | urls.forEach { 446 | processRecord(url: $0, code: code) 447 | } 448 | } 449 | } else { 450 | guard let record = try? decryptRecordFile(fileURL: url, key: decryptKey) else { return } 451 | 452 | // Ensure the change is done in the main thread as this is observed by the UI 453 | if Thread.isMainThread { 454 | code(record) 455 | } else { 456 | DispatchQueue.main.async { 457 | code(record) 458 | } 459 | } 460 | } 461 | } 462 | 463 | func mqttSettingsDidChange() { 464 | print("mqttSettingsDidChange") 465 | try? mqttClient?.syncShutdownGracefully() 466 | self.mqttClient = nil 467 | } 468 | 469 | func loadDevices() { 470 | if key == nil { 471 | _ = loadKey() 472 | } 473 | guard self.key != nil else { return } 474 | 475 | dirMonitor = DirectoryMonitor(dir: DevicesManager.RootRecordDirURL, queue: DispatchQueue.global(qos: .default)) { 476 | self.processFileChange(url: $0, flags: $1) 477 | } 478 | 479 | _ = dirMonitor?.start() 480 | 481 | processOwnedBeacon() 482 | processBeaconNamingRecord() 483 | processBeaconProductInfoRecord() 484 | processBeaconEstimatedLocation() 485 | } 486 | 487 | func processFileChange(url: URL, flags: FSEventStreamEventFlags) { 488 | switch url { 489 | case _ where url.path().hasPrefix(DevicesManager.BeaconNamingRecordDirURL.path()): 490 | processBeaconNamingRecord(url: url) 491 | case _ where url.path().hasPrefix(DevicesManager.BeaconEstimatedLocationDirURL.path()): 492 | processBeaconEstimatedLocation(url: url) 493 | case _ where url.path().hasPrefix(DevicesManager.OwnedBeaconsDirURL.path()): 494 | processOwnedBeacon(url: url) 495 | case _ where url.path().hasPrefix(DevicesManager.BeaconProductInfoRecordDirURL.path()): 496 | processBeaconProductInfoRecord(url: url) 497 | default: 498 | break 499 | } 500 | } 501 | 502 | // Function to decrypt using AES-GCM 503 | func decryptRecordFile(fileURL: URL, key: SymmetricKey) throws -> [String: Any] { 504 | // Read data from the file 505 | let data = try Data(contentsOf: fileURL) 506 | 507 | // Convert data to a property list (plist) 508 | guard let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [Any] else { 509 | throw Error.invalidFileFormat 510 | } 511 | 512 | // Extract nonce, tag, and ciphertext 513 | guard plist.count >= 3, 514 | let nonceData = plist[0] as? Data, 515 | let tagData = plist[1] as? Data, 516 | let ciphertextData = plist[2] as? Data else { 517 | throw Error.invalidPlistFormat 518 | } 519 | 520 | let sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: nonceData), ciphertext: ciphertextData, tag: tagData) 521 | 522 | // Decrypt using AES-GCM 523 | let decryptedData = try AES.GCM.open(sealedBox, using: key) 524 | 525 | // Convert decrypted data to a property list 526 | guard let decryptedPlist = try PropertyListSerialization.propertyList(from: decryptedData, options: [], format: nil) as? [String: Any] else { 527 | throw Error.invalidDecryptedData 528 | } 529 | 530 | return decryptedPlist 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /FindMyDevices/DirectoryMonitor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DirectoryMonitor.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 08/02/2024. 6 | // From https://developer.apple.com/forums/thread/115387 7 | 8 | 9 | import Foundation 10 | 11 | class DirectoryMonitor { 12 | 13 | init(dir: URL, queue: DispatchQueue, callback: @escaping (URL, FSEventStreamEventFlags) -> Void) { 14 | self.dir = dir 15 | self.queue = queue 16 | self.changeProcessor = callback 17 | } 18 | 19 | deinit { 20 | // The stream has a reference to us via its `info` pointer. If the 21 | // client releases their reference to us without calling `stop`, that 22 | // results in a dangling pointer. We detect this as a programming error. 23 | // There are other approaches to take here (for example, messing around 24 | // with weak, or forming a retain cycle that’s broken on `stop`), but 25 | // this approach: 26 | // 27 | // * Has clear rules 28 | // * Is easy to implement 29 | // * Generate a sensible debug message if the client gets things wrong 30 | precondition(self.stream == nil, "released a running monitor") 31 | } 32 | 33 | let dir: URL 34 | let queue: DispatchQueue 35 | 36 | let changeProcessor: (URL, FSEventStreamEventFlags) -> Void 37 | 38 | private var stream: FSEventStreamRef? = nil 39 | 40 | func start() -> Bool { 41 | precondition(self.stream == nil, "started a running monitor") 42 | 43 | // Set up our context. 44 | // 45 | // `FSEventStreamCallback` is a C function, so we pass `self` to the 46 | // `info` pointer so that it get call our `handleUnsafeEvents(…)` 47 | // method. This involves the standard `Unmanaged` dance: 48 | // 49 | // * Here we set `info` to an unretained pointer to `self`. 50 | // * Inside the function we extract that pointer as `obj` and then use 51 | // that to call `handleUnsafeEvents(…)`. 52 | 53 | var context = FSEventStreamContext() 54 | context.info = Unmanaged.passUnretained(self).toOpaque() 55 | 56 | // Create the stream. 57 | // 58 | // In this example I wanted to show how to deal with raw string paths, 59 | // so I’m not taking advantage of `kFSEventStreamCreateFlagUseCFTypes` 60 | // or the even cooler `kFSEventStreamCreateFlagUseExtendedData`. 61 | 62 | guard let stream = FSEventStreamCreate(nil, { (stream, info, numEvents, eventPaths, eventFlags, eventIds) in 63 | let obj = Unmanaged.fromOpaque(info!).takeUnretainedValue() 64 | obj.handleUnsafeEvents(numEvents: numEvents, eventPaths: eventPaths, eventFlags: eventFlags, eventIDs: eventIds) 65 | }, 66 | &context, 67 | [self.dir.path as NSString] as NSArray, 68 | UInt64(kFSEventStreamEventIdSinceNow), 69 | 5.0, 70 | FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents | kFSEventStreamEventFlagItemIsFile | kFSEventStreamEventFlagItemModified | kFSEventStreamEventFlagItemCreated) 71 | ) else { 72 | return false 73 | } 74 | self.stream = stream 75 | 76 | // Now that we have a stream, schedule it on our target queue. 77 | 78 | FSEventStreamSetDispatchQueue(stream, queue) 79 | guard FSEventStreamStart(stream) else { 80 | FSEventStreamInvalidate(stream) 81 | self.stream = nil 82 | return false 83 | } 84 | return true 85 | } 86 | 87 | private func handleUnsafeEvents(numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIDs: UnsafePointer) { 88 | // This takes the low-level goo from the C callback, converts it to 89 | // something that makes sense for Swift, and then passes that to 90 | // `handle(events:…)`. 91 | // 92 | // Note that we don’t need to do any rebinding here because this data is 93 | // coming C as the right type. 94 | let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer.self) 95 | let pathsBuffer = UnsafeBufferPointer(start: pathsBase, count: numEvents) 96 | let flagsBuffer = UnsafeBufferPointer(start: eventFlags, count: numEvents) 97 | let eventIDsBuffer = UnsafeBufferPointer(start: eventIDs, count: numEvents) 98 | // As `zip(_:_:)` only handles two sequences, I map over the index. 99 | let events = (0.. (url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId) in 100 | let path = pathsBuffer[i] 101 | // If we set `isDirectory` to true because we only generate directory 102 | // events (that is, we don’t pass 103 | // `kFSEventStreamCreateFlagFileEvents` to `FSEventStreamCreate`. 104 | // This is generally the best way to use FSEvents, but if you decide 105 | // to take advantage of `kFSEventStreamCreateFlagFileEvents` then 106 | // you’ll need to code to `isDirectory` correctly. 107 | let url: URL = URL(fileURLWithFileSystemRepresentation: path, isDirectory: false, relativeTo: nil) 108 | return (url, flagsBuffer[i], eventIDsBuffer[i]) 109 | } 110 | self.handle(events: events) 111 | } 112 | 113 | private func handle(events: [(url: URL, flags: FSEventStreamEventFlags, eventIDs: FSEventStreamEventId)]) { 114 | for (url, flags, _) in events { 115 | changeProcessor(url, flags) 116 | } 117 | } 118 | 119 | func stop() { 120 | guard let stream = self.stream else { 121 | return // We accept redundant calls to `stop`. 122 | } 123 | FSEventStreamStop(stream) 124 | FSEventStreamInvalidate(stream) 125 | self.stream = nil 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /FindMyDevices/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 08/02/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | var isDirectory: Bool { 12 | (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /FindMyDevices/FindMyDevices.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | com.apple.security.personal-information.location 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /FindMyDevices/FindMyDevicesApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindMyDevicesApp.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | import Foundation 10 | 11 | @main 12 | struct FindMyDevicesApp: App { 13 | 14 | let devicesManager = DevicesManager() 15 | 16 | var body: some Scene { 17 | WindowGroup { 18 | ContentView(devicesManager: devicesManager) 19 | } 20 | Settings { 21 | SettingsView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /FindMyDevices/GeneralSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralSettingsView.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 11/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct GeneralSettingsView: View { 11 | var body: some View { 12 | Text("").disabled(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) 13 | } 14 | } 15 | 16 | #Preview { 17 | GeneralSettingsView() 18 | } 19 | -------------------------------------------------------------------------------- /FindMyDevices/HomeAssistantSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeAssistantSettings.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 11/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct HomeAssistantSettings { 11 | @AppStorage("homeassistant_enabled") var enabled = false 12 | @AppStorage("homeassistant_endpoint") var endpoint: String = "http://homeassistant.local:8123" 13 | @AppStorage("homeassistant_token") var token: String = "" 14 | } 15 | 16 | struct HomeAssistantSettingsView: View { 17 | 18 | @State 19 | var settings = HomeAssistantSettings() 20 | 21 | var body: some View { 22 | VStack(alignment: .leading) { 23 | Toggle("Enabled", isOn: $settings.enabled) 24 | TextField("http://homeassistant.local:8123", text: $settings.endpoint) 25 | .textContentType(.URL) 26 | TextField("", text: $settings.token) 27 | } 28 | } 29 | } 30 | 31 | #Preview { 32 | HomeAssistantSettingsView() 33 | } 34 | -------------------------------------------------------------------------------- /FindMyDevices/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIconName 6 | AppIcon 7 | NSAppTransportSecurity 8 | 9 | NSExceptionDomains 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /FindMyDevices/MQTTSettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MQTTSettingsView.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 14/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MQTTSettings { 11 | @AppStorage("mqtt_enabled") var enabled = false 12 | @AppStorage("mqtt_server") var server: String = "" 13 | @AppStorage("mqtt_port") var port: Int = 1883 14 | @AppStorage("mqtt_user") var user: String = "" 15 | @AppStorage("mqtt_password") var password: String = "" 16 | } 17 | 18 | 19 | struct MQTTSettingsView: View { 20 | @State 21 | var settings = MQTTSettings() 22 | 23 | var body: some View { 24 | VStack(alignment: .leading) { 25 | Toggle("Enabled", isOn: $settings.enabled) 26 | TextField("192.168.1.200", text: $settings.server) 27 | TextField("1883", value: $settings.port, formatter: NumberFormatter()) 28 | TextField("user", text: $settings.user) 29 | .textContentType(.username) 30 | SecureField("password", text: $settings.password) 31 | .textContentType(.password) 32 | } 33 | } 34 | } 35 | 36 | #Preview { 37 | MQTTSettingsView() 38 | } 39 | -------------------------------------------------------------------------------- /FindMyDevices/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /FindMyDevices/SettingsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView.swift 3 | // FindMyDevices 4 | // 5 | // Created by Airy ANDRE on 11/02/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SettingsView: View { 11 | private enum Tabs: Hashable { 12 | case general 13 | case homeassistant 14 | case mqtt 15 | } 16 | 17 | var body: some View { 18 | TabView { 19 | GeneralSettingsView() 20 | .tabItem { 21 | Label("General", systemImage: "gear") 22 | } 23 | .tag(Tabs.general) 24 | HomeAssistantSettingsView() 25 | .tabItem { 26 | Label("Home Assistant", image: "HALogo") 27 | } 28 | .tag(Tabs.homeassistant) 29 | MQTTSettingsView() 30 | .tabItem { 31 | Label("MQTT", image: "MQTTLogo") 32 | } 33 | .tag(Tabs.homeassistant) 34 | } 35 | .padding(20) 36 | .frame(minWidth: 300) 37 | } 38 | } 39 | 40 | #Preview { 41 | SettingsView() 42 | } 43 | -------------------------------------------------------------------------------- /FindMyDevicesTests/FindMyDevicesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindMyDevicesTests.swift 3 | // FindMyDevicesTests 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import XCTest 9 | @testable import FindMyDevices 10 | 11 | final class FindMyDevicesTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /FindMyDevicesUITests/FindMyDevicesUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindMyDevicesUITests.swift 3 | // FindMyDevicesUITests 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class FindMyDevicesUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /FindMyDevicesUITests/FindMyDevicesUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FindMyDevicesUITestsLaunchTests.swift 3 | // FindMyDevicesUITests 4 | // 5 | // Created by Airy ANDRE on 07/02/2024. 6 | // 7 | 8 | import XCTest 9 | 10 | final class FindMyDevicesUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Airy André 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Currently incompatible with Sequoia - Apple has limited access to the password needed to decrypt the FindMy files** 2 | 3 | Basic app to send FindMy devices and items (AirTag) locations to Home Assistant 4 | --------------------------------------------------------------------------------