From e667af92010ea93eaf40a1d07b3c5020acb5996f Mon Sep 17 00:00:00 2001 From: William Bruno Date: Tue, 14 Apr 2026 09:53:52 -0400 Subject: [PATCH] refractor --- .idea/pymd3_vue_location_sim.iml | 2 +- cookies/willbrunogmailcom.cookiejar | 38 +- cookies/willbrunogmailcom.session | 2 +- geocache.db | Bin 28672 -> 57344 bytes src/pymd3_vue_location_sim/database.py | 44 - src/pymd3_vue_location_sim/db_models.py | 42 - src/pymd3_vue_location_sim/geo_cache.py | 57 +- src/pymd3_vue_location_sim/icloud_monitor.py | 130 +- .../locationsimulation.py | 213 --- src/pymd3_vue_location_sim/models.py | 71 +- src/pymd3_vue_location_sim/routes/api.py | 379 ------ src/pymd3_vue_location_sim/routes/socketio.py | 425 ------ src/pymd3_vue_location_sim/server.py | 1173 +++++++++++------ 13 files changed, 947 insertions(+), 1629 deletions(-) delete mode 100644 src/pymd3_vue_location_sim/database.py delete mode 100644 src/pymd3_vue_location_sim/db_models.py delete mode 100644 src/pymd3_vue_location_sim/locationsimulation.py delete mode 100644 src/pymd3_vue_location_sim/routes/api.py delete mode 100644 src/pymd3_vue_location_sim/routes/socketio.py diff --git a/.idea/pymd3_vue_location_sim.iml b/.idea/pymd3_vue_location_sim.iml index 73f5b78..d174d5c 100644 --- a/.idea/pymd3_vue_location_sim.iml +++ b/.idea/pymd3_vue_location_sim.iml @@ -1,5 +1,5 @@ - + diff --git a/cookies/willbrunogmailcom.cookiejar b/cookies/willbrunogmailcom.cookiejar index a7e97d9..107f95c 100644 --- a/cookies/willbrunogmailcom.cookiejar +++ b/cookies/willbrunogmailcom.cookiejar @@ -1,25 +1,25 @@ #LWP-Cookies-2.0 Set-Cookie3: dslang=US-EN; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0 Set-Cookie3: site=USA; path="/"; domain=.apple.com; path_spec; secure; discard; HttpOnly=None; version=0 -Set-Cookie3: acn01=VXsJ1FolypnjIirlSDusIR9ovu+CG7pXMkEM9wALeBAzs3+u; path="/"; domain=.apple.com; path_spec; secure; expires="2027-04-05 02:36:14Z"; HttpOnly=None; version=0 -Set-Cookie3: aasp=9968286574C1B31BE2158A19285C92934F9213370AF67665FD3DC55BCB20C2B39E8728B5D8B4952B5F8B8B09F4D1C6826058791E379DE064B11DD79D0F7F1F87A27809C8C1CF291EEC649A8495D06747AA26022E3C56113F2E43528B8F75ECA9A7FA5EADDBFA510B80AF6C0198C74B431BA8BA5E76EA6411; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0 +Set-Cookie3: acn01="NNc+TvoQfp2pQiuHIAAqUBtXR1S4XhqZJWcHCAARBs+41xk/"; path="/"; domain=.apple.com; path_spec; secure; expires="2027-04-14 09:01:23Z"; HttpOnly=None; version=0 +Set-Cookie3: aasp=DE9DF160D458CB33DB61E598CA924CC24441BCA91527DC0B793F5576DC6E8DF8BDBF5D9424A9243220F7D62057DD673EE28B6F754666DDF1949F07205B0E0BC0066D0CEDEA32E34965B3EA5AC66D2F90BEAC5E289B72B338B05E5D57B8DDD402BBC24F218664008D4D70C62F90DF6411A599DFC6DE59BA38; path="/"; domain=.idmsa.apple.com; path_spec; secure; discard; HttpOnly=None; version=0 Set-Cookie3: DES580750186337023c50d1415a6e6ca44a2="HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"; path="/"; domain=.idmsa.apple.com; path_spec; secure; expires="2026-04-21 03:09:12Z"; HttpOnly=None; version=0 Set-Cookie3: X-APPLE-UNIQUE-CLIENT-ID="\"BA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnRzaIRDmdzLmljbG91ZC5hdXRovQCklMXNjS27UT7gDS-gYQ89116WefHaUfyFRHCo9vj0gIvottHkbhRRXrty62DAgu_MJaMZUjxNBFe0CwGCcMtgC4Lg6NaNaHBlBlsy-OAjkFv1sJqr_nax-L7L3P6u9vXYy9jjrvy5Bl47wGJmZgnMbee6RQ~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGnRzaIRDmdzLmljbG91ZC5hdXRovQCklMXNjS27UT7gDS-gYQ89116WefHaUfyFRHCo9vj0gIvottHkbhRRXrty62DAgu_MJaMZUjxNBFe0CwGCcMtgC4Lg6NaNaHBlBlsy-OAjkCBWl1oaHvC06H_6zbXfJv5b7ws2aI9JUCBGIrlYvdtyArGvxA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-LOGIN="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGneOZkRDmdzLmljbG91ZC5hdXRovQCaguVTZqS1WtbK-2fXiMlSEneJRuCgMGur1kfGcotuhS8Kvhra-S8EJ_LDYfFgbF8KYm2Dwug7ev54fe7_F-ui_zK0AqM9w7i8HvwWtWOA6QeYJtzFKgl7BVt_c5I4FrExr468XbgzmKtoXQxDTJu_wOLbbA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-VALIDATE="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGneOZkRDmdzLmljbG91ZC5hdXRovQCaguVTZqS1WtbK-2fXiMlSEneJRuCgMGur1kfGcotuhS8Kvhra-S8EJ_LDYfFgbF8KYm2Dwug7ev54fe7_F-ui_zK0AqM9w7i8HvwWtWOA6amlkIysG_APwQqmrfcDi-L_p8u1i1A8tqFrDUUfJs21Y1v8oA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-USER="\"v=1:s=1:d=157320350\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 Set-Cookie3: X_APPLE_WEB_KB-FHMLYL_TPMN_3A8D3KIPPI0C_EC="\"v=1:t=BA==BST_IAAAAAAABLwIAAAAAGm_OLQRDmdzLmljbG91ZC5hdXRovQD38nYoxQenHW9WggeFKkoDa8I8zeKoOshv6I4dsZQalR2itry1r6kUZe9d_BZan1W-oKlImTrYi_-Vt5Q4YEJWJITWeqN8QChxvbTXB0o8sQ-wAIzBL1J5sQIRBqMadrtP5U0wslkRg0u0AguK20CM4TGoGg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-21 00:32:52Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-DS-WEB-SESSION-TOKEN="\"AQGj4lLXV42fukxli9in3ovKoBpXDePY1U3TMHAECRIIVd+zcdKkZDdg0hx6yPg+bVVZDVKN5iqJGKZsbIGATe6G/ce1fuxw+aON29I3TuOIuWrLNQyy1Idjn6SzX8RWY5MlFmoM+oVFt4MqdtvYJgGSfGv1IAe8CfTWPyQhqlk4qGgczVCCx74ReWAz7mh0GQk3aFwre6RK3QpJV1sgFGLh5uu0Jw2ThzgGfsYlORVDtz/90OjWuGMO6fr8BeXAu5Oh3mgryYTM39Et/Pc/8GuW9beXXI1WLul1eyQ/rK3xd8iZcEwYSFeBYfjNRYFAS3raJkIC0DJkKd93baYUNhEGJHXNtb6uzrueFmQT+svVA8VXY79IG9noP0S+0Ie3oDs0rZVwz5dHZhIzCEepxFz+WtAuIrkUXOFk71wkytU+oAdA90qfPP/DzeXbKGFbHgiBkoxSxsKsGpikC3U0ne6bPWcJZIsLh1vlcpNutXJGkAqm4VXYBGWbaWqr2UJyDBxJQClRngaKR/VB9U+BFvK5NylHsU0gzv7U8goPnQBr62ZAEmD3TDTCxuqIbbgaRxYRJD4smOVemXktlhwV8FvcH1r/UonCGal9YedLDLzL9bxMPg9vZVKnaAMDKwsJstLDFzY4EB2IS1Ga/XJSfO85MFPe6SeNOU2v2how1RyjoTeKhvACva16kcmT/RpUFR9W8qruLadc8sGGGXZ/LrpwG76hZBBrHef1jrsZw2lagAhtT7p69Sr7V9WQgZKoRq8ixSllm5VX1TJqoHkizS6lOBCOeasVT8g=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-07-04 02:49:06Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Events="\"S2V5QXBwbDoBAAAA8QQzAADzWe4VyeK8B8xw8IwuUAj3/m/Tnx3zxoa+V62xZNyypQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Documents="\"S2V5QXBwbDoBAAAAAgQzAABmoTT+n0/L3H+DLCJo23ecPgEOXkHSr8swBUva2awlRw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Photos="\"S2V5QXBwbDoBAAAAAwQzAACZIqX0p5ijwnU4kYUFAd6s41Ki8Ll00cb5+m/HALArxA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Cloudkit="\"S2V5QXBwbDoBAAAABAQzAABlviOyJnRiHxxWxB8ItTVk+oDDvc7OUX1eJkqX0bYdXA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Safari="\"S2V5QXBwbDoBAAAAFgQzAACET30lpsAp5WwxJGxde2g5ajvsMowlnYgU2tfOoebjgA==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Mail="\"S2V5QXBwbDoBAAAABwQzAACaoZ2L80C9Pl1HRn65m+rka24bPiCumxBL/I07rSfkGQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Notes="\"S2V5QXBwbDoBAAAACQQzAAA5vanrsd/XLqXzVPYQGtt68uOMqCgEcW8J8pisVkU8xw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-News="\"S2V5QXBwbDoBAAAACwQzAADVWJymh0KvnbnDDHZBntjVHLnfcTr/Q0ES5n8RjkStNg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-PCS-Sharing="\"S2V5QXBwbDoBAAAADAQzAADpgr9x39VeWGIvzow7K8TzwvGLETRpkbcQH4Q8gKed2w==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-05 02:49:09Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGnRztkRDmdzLmljbG91ZC5hdXRovQDHdf4C78ZKILT8BBTIOy2E0pByyyFr6yZauanzkyx8PQqrJBuXKi3q6Ms5vECA80g_yAB1WH-0ep0fExYB60bSXj-tV6WHYlFPew3HCW9LTJGs4uea0vlYHya2rKBlXbmkU3-WnL6eHN_U6MuYKO1zefaf8A~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-19 02:54:17Z"; HttpOnly=None; version=0 -Set-Cookie3: X-APPLE-WEBAUTH-FMIP="\"BA==BST_IAAAAAAABLwIAAAAAGnRztkRDmdzLmljbG91ZC5hdXRovQDHdf4C78ZKILT8BBTIOy2E0pByyyFr6yZauanzkyx8PQqrJBuXKi3q6Ms5vECA80g_yAB1WH-0ep0fExYB60bSXj-tV6WHYlFPew3HCW9LTFFwE029MTNgDRSz7RG3UgEKnrs8iiy5GqIVN5vkstidJ8ogxA~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0 -Set-Cookie3: xr_3n2093n1a="f16+Jo6W6qPJmRkZzBixaQfuGOyKO+kJ4iSEZyG94A=="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-DS-WEB-SESSION-TOKEN="\"AQFfzSjK/WZszw0Fsqj3EgUNWNY4t+SKa9A0tAZvx+6URTf4eVhwhvkiDp7K92hEHTDNkvBVpeLWCOciQyzVLPcDhnss5IUImi6rDXxU6diem5bTWiznw50LWVBDnC9GnFB1ER6z1jqvw6u/2pDoO7slxsylU2j5fDp6mPvMe2iz83UhRRDLqUV5RjHOZ3Y93rnGSskislOzvvd/wuWBL9aXdC22oeGCs8lALzT2LDTJE/fJfw4H5ttKDoh5sA5bPgO3UjBtvAOMurTwdlYHmyaZ5pPrRDKSl9sgcbzkxwwvbvCVUbbsLt7rUolhsJmY33Pu51A7ubiG6hzIEg8U/mAQH7A9Ill4D/HbpR0Q4/uW3Sw9BkdI2MopLsLLJudBW66IBfYSGY53Dzr33IXjyi8bn+f8drCoxv+Jqsut5Yn/MI8Ooc9vpkON2INj+MDPCbr9LP2dh2hjOjvyHWJvDIYuIa/1ols9fx7GAT37dp8iuJYq7UYUT4GVaayt5xRH/a+hE8IpM+6gMAVgXMYBYi40eAmvcCG1+Pd4NKTQGRio+Z53bfcpMq4IjV5ZpwmcfWHMsCs6UNuFzDUhz3Oh0O9zK1hubuxgP4NvpJWZTGeDxm3Ak4nphbELvoFeRzqzX+wHLIKfr8H/ruK8LCrLeJvt4QAExQH9N6pjBDot0M/WpG0aZ0ZbOubqhfmX5KftG2QKJwDfLouyLeWp07TvyAsWXkS+o5Bc2ararFO8R5+OHURIIx9O47UUHkA0a7C1iYhdbxf8MMhOjYt6J8buormR4LDZEp2q5Xk=\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-HSA-TRUST="\"28a33818a1dce9a0eecde38e7c8fcc6f080b70bc9feb505599fb2855903a4792_HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-07-13 12:56:57Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Events="\"S2V5QXBwbDoBAAAA8QRQAABCqtxc6DiNgfM7sfluuTm75zY6t8HcSbX5byVn83L6Yg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Documents="\"S2V5QXBwbDoBAAAAAgRQAAC9HDQLy/UwkOt+Z+OPhwCJxnyB1byDPGL1UYWgHtJEgg==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Photos="\"S2V5QXBwbDoBAAAAAwRQAACxKufqce7i/IW850g8JkJ73J0Wyxpjpfh5nAk7eioyvw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Cloudkit="\"S2V5QXBwbDoBAAAABARQAABnmo+VJgPhrzid+Xl/01UA02nJiwkk9q0MBvy6QH758g==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Safari="\"S2V5QXBwbDoBAAAAFgRQAAC9tTzH5+GZyvSdFF1r+Zmvj/kJmQNWTYNLCmNG8tX6Cw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Mail="\"S2V5QXBwbDoBAAAABwRQAABywaR34zLT3fm0XcBUYCbETV16EkpVL6/O4hB9EHwDcQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Notes="\"S2V5QXBwbDoBAAAACQRQAADqrnrINYzUlz7QoxZO+ami+5QY9Qtys3wAuCPjM0x7wQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-News="\"S2V5QXBwbDoBAAAACwRQAAAYQEni07N+B3hrogj/ZlPdWE3ibkkBCYJcDgadmwRzOw==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-PCS-Sharing="\"S2V5QXBwbDoBAAAADARQAABGcfw9nr8NjONRvLxWcwgMoaBXj7xode3Sf7ajDnV2IQ==\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-05-14 12:57:00Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-TOKEN="\"v=2:t=BA==BST_IAAAAAAABLwIAAAAAGneP2oRDmdzLmljbG91ZC5hdXRovQCgEswrYhaJx11ybTgoBMzcGPh9PUsXyVTJCpHvMijnTIkcF0JOFbgPAg8KKzEH9Rd9ebBJmHwfJP5LUVHG1YaUgB5xL9LX2AQYoGwnJcaj2mCD7LQxw4WCZSVJf9TauwpgZlhHpR15qHwnpiIC3Qg8Rue57g~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; expires="2026-04-28 13:21:46Z"; HttpOnly=None; version=0 +Set-Cookie3: X-APPLE-WEBAUTH-FMIP="\"BA==BST_IAAAAAAABLwIAAAAAGneP2oRDmdzLmljbG91ZC5hdXRovQCgEswrYhaJx11ybTgoBMzcGPh9PUsXyVTJCpHvMijnTIkcF0JOFbgPAg8KKzEH9Rd9ebBJmHwfJP5LUVHG1YaUgB5xL9LX2AQYoGwnJcaj2h-pjNdL9bRPA229-vtr8LU4Syp2u2FO2pDcJ6aasJg2FHdtQg~~\""; path="/"; domain=.icloud.com; path_spec; domain_dot; secure; discard; HttpOnly=None; version=0 +Set-Cookie3: xr_3n2093n1a="B50g9gcKmoM+KF+j8hPX6wKXv22QcYsBKEONZTiNR4g="; path="/"; domain=p144-fmipweb.icloud.com; path_spec; secure; discard; HttpOnly=None; version=0 diff --git a/cookies/willbrunogmailcom.session b/cookies/willbrunogmailcom.session index f64df39..70c7566 100644 --- a/cookies/willbrunogmailcom.session +++ b/cookies/willbrunogmailcom.session @@ -1 +1 @@ -{"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "9968286574C1B31BE2158A19285C92934F9213370AF67665FD3DC55BCB20C2B39E8728B5D8B4952B5F8B8B09F4D1C6826058791E379DE064B11DD79D0F7F1F87A27809C8C1CF291EEC649A8495D06747AA26022E3C56113F2E43528B8F75ECA9A7FA5EADDBFA510B80AF6C0198C74B431BA8BA5E76EA6411", "auth_attributes": "Xmk4qpyEj50xx+3lPjCdsfTj8wLQuUJ9ZlW4t+MjKa0jiHZ9YmQQJfUGKqW0Jl3jpyX0m4eYPaS7oSPeWR3jUx9jjrdLW3fuUjZrksYarxCZGX8/V1ArrgSzl8lsbD9MSNDxtBDEyJZSlnp4syp2/Tqs2jM/dE3641Pdyh4ScxQi9S1c4HtHnn0ryjT3XcH034UwQnGtnLnJS8FXtWAjFqPUv0I9C06BVPLkI7jw+I/efCr0sMYUsPgclZbSVuPU7+ioZPJeeV5xJm/dGQpoAAt4ED/Fik8=", "scnt": "AAAA-jk5NjgyODY1NzRDMUIzMUJFMjE1OEExOTI4NUM5MjkzNEY5MjEzMzcwQUY2NzY2NUZEM0RDNTVCQ0IyMEMyQjM5RTg3MjhCNUQ4QjQ5NTJCNUY4QjhCMDlGNEQxQzY4MjYwNTg3OTFFMzc5REUwNjRCMTFERDc5RDBGN0YxRjg3QTI3ODA5QzhDMUNGMjkxRUVDNjQ5QTg0OTVEMDY3NDdBQTI2MDIyRTNDNTYxMTNGMkU0MzUyOEI4Rjc1RUNBOUE3RkE1RUFEREJGQTUxMEI4MEFGNkMwMTk4Qzc0QjQzMUJBOEJBNUU3NkVBNjQxMXwyAAABnVuNNE9ZApXl4PMBXcfJlCl96p_1cCfBRhHOdT0BkG04tbOjDgF3V8vlM9CkAAt4ECMQx8n6Pfm37fS8OCpOsUoTL7pc1A-DXplPGubA4CDTzbZfkw", "account_country": "USA", "session_token": "YaPd8Iuy2R5+ZsjKRO52duQIvtCutVY0Mf8UTjNowXeyq6E8O+VBbYFMEh1oq/bQIlTmu0zYRc/79K6Qt8jTYpuQArQNhVDqCkUNpEgc0wyKA3L15xFP11t7/nDDlzL1U0hU2JEpiXcNpr8oXF0oJxDf+p9zRA+ryHL/jybhiiaVX48zUB6cYc8m++GA+Oid2r45sftOPRUkYpgIF2UwIq0pF9xat810EDnaDTVXYe2geQ2Vt9BNJiAJLWNA3WPfifMpAZzN5b2vHg8Nsh3e/Nm/+GQvjrtcBr5vT3RwlslOfWIqJbtoLL/np16f+szHM6e5ZOxejMdOifG5PxfPshr+qMzu/HRX7Ex8r6975KaiW98MBrvbVITsgsL/YmutmdCAFfATXYxu2JOfnOHhVPDANNRLh8N1ZTSJtGY3fXP3DxbNlCjmCA3JAMWMiUkhlxDcA4xPubDiiU5ZNJ6x1ckkAdjtQkOvjESt9N3ydMf89sz9WDqdps0ll1sYXINjLM1OagoBSxzE0y0szkX34YkCki0j3f25OH+utNCNIDZV9fWTEYiINOY1POSX07ZPbzw2AKfVqJP3DduVL4rzjXTIcGtTBkcr04c0Lz43knqcPkVQI1QNckxiFL1owcZEdlxoPKIOL8ojXRK7b6NAJyk+t5vLpDWrt97bHegGaJoe7swSYP75rSxkVdh8vAczd/Mir4OmB/uerwtEZq9KUiKWvC52mYYolF/XYazJKAVGx1FpywM8DL5mHlbcJzTUR2NBWRD/b0/m73Jxs32kLButuWtnKXiHk/OqC1JZfA8+LNdNaK6KtJcdEul3go7LCwuxQZ4o8o2QzN8SqgM9UUvcyMf/3LwDoZgRdPmKNRN6GV5a3J9I+WF+Jz98W0Klbn5PJa6y0Zc9aaUy1H6Ho6pdzYeLdMMhwJ5RaJqvgnHxDFYU2UQKiFt3w26gFANk1AALeBBE/iN7", "trust_eligible": "true", "grant_code": "c3254a0d3cdf4487fa423d13cca72142f.0.przwz.C6giZgZ9fLof99ebfJFxDA", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"} \ No newline at end of file +{"client_id": "a803c3ce-2586-11f1-a724-8f6777a1d2b5", "session_id": "DE9DF160D458CB33DB61E598CA924CC24441BCA91527DC0B793F5576DC6E8DF8BDBF5D9424A9243220F7D62057DD673EE28B6F754666DDF1949F07205B0E0BC0066D0CEDEA32E34965B3EA5AC66D2F90BEAC5E289B72B338B05E5D57B8DDD402BBC24F218664008D4D70C62F90DF6411A599DFC6DE59BA38", "auth_attributes": "zahj2u9cli9LAxaDkM/LZ+cVDLo7WY7x9DXKo/GGGAb+VYFvMJT/jxRGAHy2AaUN3MbqtRLU0i7jA2oeXhQ1iMaUEntaTv9AWTsTfylbAnoJ1Vo+euKP22hW6V/IbTfClCFMHGxZul3ooBdwJwz+otrAo3VPF3yUptZPvP5+7JOegMRDA1awse9Ci0QtcOfI8p6tyj5aV1xjcpLbt29wdmV+Ny/Lm25iFpDKnf/XIgvT1MUXIpGot1NfW3rqWViq/M/pq8DK0i8/RuLDevFYABEGz8Qt3PE=", "scnt": "AAAA-kRFOURGMTYwRDQ1OENCMzNEQjYxRTU5OENBOTI0Q0MyNDQ0MUJDQTkxNTI3REMwQjc5M0Y1NTc2REM2RThERjhCREJGNUQ5NDI0QTkyNDMyMjBGN0Q2MjA1N0RENjczRUUyOEI2Rjc1NDY2NkRERjE5NDlGMDcyMDVCMEUwQkMwMDY2RDBDRURFQTMyRTM0OTY1QjNFQTVBQzY2RDJGOTBCRUFDNUUyODlCNzJCMzM4QjA1RTVENTdCOERERDQwMkJCQzI0RjIxODY2NDAwOEQ0RDcwQzYyRjkwREY2NDExQTU5OURGQzZERTU5QkEzOHwyAAABnYtHDOAIdGLN7q_QgI9AB0GH5OFan9jwoX6JhEYkfqH0K-ydWfBkwqF6Le3-ABEGz6vfU7IlwYBHKE9RVBG8MeYu5XjWFmHIzPcRjW3ImaknBIL7cA", "account_country": "USA", "session_token": "cD1YUhA3Q+BFVazN3KBXOKbVHASxhLLJyWAqVySi3kNzZY4W8ikOiA+sWjs3ROhJcgQh2aErAhxbB3MFVq0h1rdhwbfe6pDtiC1AO7gjuDjCjEJyfZXr4Z5YpeI0ETekbJnNtFrOvxQ5eMdBpXEf9GGOD8FSkBNkl1XYZWdBbsFPKrmGMPEBWcv07pD1XzkONFiwQs71dLPTEwbImkKMjYtTUckv61hjDV9+ktZrYRmRjEhclp3nXkB4a49d88pNVTt9+Dm2MGIuxPvjKogTmE6rLhhxQavVjJrc16zSayrXv30qNiKJfckwSCfcliqWEgICB/I56ZALxbAmvMBWOdB9YPYfvle81KfI1UGrOiXko3G7IzPAA/7voNZqStR0z8FiTiua3GxorxDmUFHezp1kpLgpa+yzNyBobbLP3vbSRC6083rN0XbXM/ad7Tmm2G8FDyzM4XOrE+P2X5mtQcXxlba7ygKq51nP4vM1YrfQYc6ZRgRBJK+vA21i3igPA8awj9zrg4KF5GvMGjwSZ6Tp3wG6MSws909+80bvdp5x22b88k/sjzk2MLNjborCRPz9xC9H/ZAeqjPp6F7S/AqwwEa6RQ34ykiZVLA5dv5bNFteEQZNVO6FmoDBecw3BscsBt6MAHA50mUjq6dvzeZxOOOlpGtgJyOWybyfK3VoevZ03utoYg12dTvLMwGaIyELPhOP6mJFDa2bJxmeDPwkkt+bwk3EVumRudUAhOcIbxXJ52Zt4HuvgzDE/6c+bg9Jbv29cw5njr2P+8VYJO4ghFqmnan+QLnFiwP7Ypacc+Jl0YbEB1bBa/paAnD0wlzYzqS8qi+GQpD5Ro4BFkbS7hMPIvZL1+n4xC3sepc3AAq+0IP6PerQiUn6ORK88JYuRnnMUuwEJcpDtzL/CONusB75h9R4KFIQZl9GykYQ3ZLj+3JerAgvzcFOR0D1OQARBs/I9osU", "trust_eligible": "true", "grant_code": "c610a05475fb544cab5128ff1b920b9a6.0.srzwz.U70T2OR3TxSYAjcY4DHZ9w", "trust_token": "HSARMTKNSRVXWFlajR2ecD1662phQjqU9vXxnL49ZjypuVYYXHDpA3wTiX6Mf2J4WDlIhZj52z81aDOuz+VC80bVhV41TSNN4ggoPjW8WnsQrjniTQYkgJycPQNnzhkK4hfe2AMrr/bhrJJm8sHHc+Oh1HUckN6T7T4c1bmf2Qg9tRwsdRDNyMMyFH/Ml/cQlWKj39/YHlY=SRVX"} \ No newline at end of file diff --git a/geocache.db b/geocache.db index fa4bfd2dd280765bd5978c62249a1a47d1487dec..de642ea349c96c1f97d4fe7ecd94ea8ee0d2d858 100644 GIT binary patch literal 57344 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 U-y||fbf(4@W8?Q>bt)+T3q(g^#sB~S delta 201 zcmZoTz})bFae_3fB?AKk`$PqMM$3%}OZfSi`9A^$7})uL^MB+2xLL^HKL6y8^1}5N zTt6AObGVziXK`=jKEZvT`y2md{yqGj{7bn7_!Ic#xI_7pd2jMg;bZ6F<@4cL$8(hD z4vz}AI`3-ULp+~(GI_ap8hEDj1o2q&mvjB({lI6+ZO3oK|B7FlzmHdu?;39{ZyK*Z x-+sOoycT?K`C0fT@>PKx0tFju%D6WF)Z#W}=3|^UnLR;nV^JmJ=6Q)pd;qzdH}C)e diff --git a/src/pymd3_vue_location_sim/database.py b/src/pymd3_vue_location_sim/database.py deleted file mode 100644 index 5d96996..0000000 --- a/src/pymd3_vue_location_sim/database.py +++ /dev/null @@ -1,44 +0,0 @@ - -from typing import List -from sqlalchemy import select -from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker -from .db_models import Base, Location, Route, Waypoint - - -DATABASE_URL = "sqlite+aiosqlite:///.locations.db" -engine = create_async_engine(DATABASE_URL, echo=True) -async_session_local = async_sessionmaker(engine, expire_on_commit=False) - -async def init_db() -> None: - async with engine.begin() as conn: - await conn.run_sync(Base.metadata.create_all) - -async def create_location(name: str, address: str, latitude: float, longitude: float, is_favorite: bool = False): - async with async_session_local() as session: - new_location = Location(name=name, address=address, latitude=latitude, longitude=longitude, is_favorite=is_favorite) - session.add(new_location) - await session.commit() - return new_location - -async def get_locations(): - async with async_session_local() as session: - result = await session.execute(select(Location)) - return result.scalars().all() - -async def create_route(name: str, origin_id: int, destination_id: int, waypoints_data: List[dict]): - async with async_session_local() as session: - new_route = Route(name=name, origin_id=origin_id, destination_id=destination_id) - for wp in waypoints_data: - new_route.waypoints.append(Waypoint(order=wp['order'], description=wp['description'])) - session.add(new_route) - await session.commit() - return new_route - -async def get_routes(): - async with async_session() as session: - # Use joinedload for efficient loading of relationships - from sqlalchemy.orm import joinedload - result = await session.execute( - select(Route).options(joinedload(Route.waypoints)) - ) - return result.scalars().unique().all() diff --git a/src/pymd3_vue_location_sim/db_models.py b/src/pymd3_vue_location_sim/db_models.py deleted file mode 100644 index 7c1028f..0000000 --- a/src/pymd3_vue_location_sim/db_models.py +++ /dev/null @@ -1,42 +0,0 @@ - -from sqlalchemy import ForeignKey, String -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship -from sqlalchemy.ext.asyncio import AsyncAttrs -from typing import List - - -class Base(AsyncAttrs, DeclarativeBase): - pass - -class Location(Base): - __tablename__ = "locations" - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(100)) - address: Mapped[str] = mapped_column(String(255)) - latitude: Mapped[float] - longitude: Mapped[float] - is_favorite: Mapped[bool] = mapped_column(default=False) - - routes: Mapped[List["Route"]] = relationship(back_populates="destination", cascade="all, delete-orphan") - - -class Route(Base): - __tablename__ = "routes" - id: Mapped[int] = mapped_column(primary_key=True) - name: Mapped[str] = mapped_column(String(100)) - origin_id: Mapped[int] = mapped_column(ForeignKey("locations.id")) - destination_id: Mapped[int] = mapped_column(ForeignKey("locations.id")) - destination: Mapped["Location"] = relationship(back_populates="routes") - - waypoints: Mapped[List["Waypoint"]] = relationship(back_populates="route", cascade="all, delete-orphan") - - - -class Waypoint(Base): - __tablename__ = "waypoint" - id: Mapped[int] = mapped_column(primary_key=True) - order: Mapped[int] - description: Mapped[str] - route_id: Mapped[int] = mapped_column(ForeignKey("route.id")) - route: Mapped["Route"] = relationship(back_populates="waypoints") - diff --git a/src/pymd3_vue_location_sim/geo_cache.py b/src/pymd3_vue_location_sim/geo_cache.py index c4c1c8d..1a3d593 100644 --- a/src/pymd3_vue_location_sim/geo_cache.py +++ b/src/pymd3_vue_location_sim/geo_cache.py @@ -6,15 +6,21 @@ from geopy.adapters import AioHTTPAdapter from geopy.extra.rate_limiter import AsyncRateLimiter logger = logging.getLogger("ios-api") +CACHE_LOOKUP_SQL = "SELECT address FROM location_cache WHERE lat_lon = ?" +CACHE_UPSERT_SQL = "INSERT OR REPLACE INTO location_cache VALUES (?, ?)" class AsyncReverseGeocoder: - def __init__(self, db_path="geocache.db", user_agent="pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)"): + def __init__( + self, + db_path: str = "geocache.db", + user_agent: str = "pymd3_vue_location_sim/0.1.0 (iam@williambr.uno)", + ): self.db_path = db_path self.user_agent = user_agent self._init_db() - def _init_db(self): + def _init_db(self) -> None: """Initializes the SQLite database.""" with sqlite3.connect(self.db_path) as conn: cursor = conn.cursor() @@ -26,19 +32,29 @@ class AsyncReverseGeocoder: ''') conn.commit() - async def get_address(self, lat, lon): - """Reverse geocode with caching.""" - key = f"{lat:.5f},{lon:.5f}" - logger.info("Checking location_cache for %s", key) - # Check Cache - with sqlite3.connect(self.db_path) as conn: - cursor = conn.execute( - "SELECT address FROM location_cache WHERE lat_lon = ?", (key,)) - row = cursor.fetchone() - if row: - return json.loads(row[0]) + @staticmethod + def _cache_key(lat: float, lon: float) -> str: + return f"{lat:.5f},{lon:.5f}" + + def _read_cached_address(self, key: str) -> dict | None: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.execute(CACHE_LOOKUP_SQL, (key,)) + row = cursor.fetchone() + return json.loads(row[0]) if row else None + + def _store_cached_address(self, key: str, address_data: dict) -> None: + with sqlite3.connect(self.db_path) as conn: + conn.execute(CACHE_UPSERT_SQL, (key, json.dumps(address_data))) + conn.commit() + + async def get_address(self, lat: float, lon: float) -> dict | None: + """Reverse geocode with SQLite cache.""" + key = self._cache_key(lat, lon) + logger.info("Checking location_cache for %s", key) + cached_address = self._read_cached_address(key) + if cached_address is not None: + return cached_address - # Fetch New Data async with Nominatim( user_agent=self.user_agent, adapter_factory=AioHTTPAdapter @@ -49,15 +65,10 @@ class AsyncReverseGeocoder: location = await reverse(key) if location: logger.info("Nominatim response: %s", location) - address_data = location.raw['address'] - # Save to Cache - conn.execute( - "INSERT OR REPLACE INTO location_cache VALUES (?, ?)", - (key, json.dumps(address_data)) - ) - conn.commit() + address_data = location.raw.get("address", {}) + self._store_cached_address(key, address_data) return address_data return None - except Exception as e: - print(f"Error: {e}") + except Exception: + logger.exception("Reverse geocoding failed for key=%s", key) return None diff --git a/src/pymd3_vue_location_sim/icloud_monitor.py b/src/pymd3_vue_location_sim/icloud_monitor.py index 0193657..f3b1cc8 100644 --- a/src/pymd3_vue_location_sim/icloud_monitor.py +++ b/src/pymd3_vue_location_sim/icloud_monitor.py @@ -2,6 +2,7 @@ import asyncio import logging import os import sys +import textwrap from collections.abc import Callable import click @@ -16,10 +17,17 @@ from pyicloud.exceptions import ( PyiCloudPasswordException, ) -from .models import iCloudReturnData +from .models import ICloudReturnData load_dotenv() logger = logging.getLogger("ios-api") +COOKIE_DIRECTORY = "./cookies" +ENV_APPLE_ID = "APPLE_ID" +ENV_APPLE_PW = "APPLE_PW" +ENV_SELECTED_DEVICE_ID = "SELECTED_DEVICE_ID" +ENV_SELECTED_DEVICE_NAME = "SELECTED_DEVICE_NAME" +ENV_AUTH_INIT_TIMEOUT = "ICLOUD_AUTH_INIT_TIMEOUT_SECONDS" +BACKOFF_SCHEDULE = (15, 30, 60, 120, 300) AUTH_EXCEPTIONS = ( PyiCloudAuthRequiredException, PyiCloudFailedLoginException, @@ -38,11 +46,11 @@ class FindMyMonitor: get_client_sids: Callable[[], list[str]] | None = None, code_timeout_seconds: int = 180, ): - self.username = os.getenv("APPLE_ID") - self.password = os.getenv("APPLE_PW") + self.username = os.getenv(ENV_APPLE_ID) + self.password = os.getenv(ENV_APPLE_PW) self.token_file = token_file - self.selected_device_id = os.getenv("SELECTED_DEVICE_ID") - self.selected_device_name = os.getenv("SELECTED_DEVICE_NAME") + self.selected_device_id = os.getenv(ENV_SELECTED_DEVICE_ID) + self.selected_device_name = os.getenv(ENV_SELECTED_DEVICE_NAME) self.selected_device = None self.queue = queue self.api = None @@ -52,13 +60,33 @@ class FindMyMonitor: self.get_client_sids = get_client_sids self.code_timeout_seconds = code_timeout_seconds self.auth_init_timeout_seconds = int( - os.getenv("ICLOUD_AUTH_INIT_TIMEOUT_SECONDS", "0") + os.getenv(ENV_AUTH_INIT_TIMEOUT, "0") ) self._logged_candidates = False self._no_location_streak = 0 self._auth_error_streak = 0 self._fetch_lock = asyncio.Lock() + async def _create_api_session(self, has_token: bool) -> bool: + try: + init_args = [self.username] + if not has_token: + init_args.append(self.password) + init_task = asyncio.to_thread( + PyiCloudService, *init_args, cookie_directory=COOKIE_DIRECTORY + ) + if self.auth_init_timeout_seconds > 0: + self.api = await asyncio.wait_for( + init_task, timeout=self.auth_init_timeout_seconds + ) + else: + self.api = await init_task + return True + except Exception: + source = "cookies" if has_token else "credentials" + logger.exception("Failed to initialize iCloud session from %s", source) + return False + async def _request_code_from_vue(self, prompt: str) -> str | None: if self.sio is None or self.get_client_sids is None: logger.warning("2FA request skipped: Socket.IO context not configured") @@ -91,7 +119,7 @@ class FindMyMonitor: logger.warning("Invalid 2FA code payload from sid=%s", sid) return None - async def authenticate(self): + async def authenticate(self) -> bool: """Authenticates with iCloud, handling 2FA and token storage.""" if not self.username: logger.warning("APPLE_ID is not configured; skipping iCloud monitor authentication") @@ -103,42 +131,15 @@ class FindMyMonitor: ) return False - if os.path.exists(self.token_file): - print("Loading stored session...") - try: - init_task = asyncio.to_thread( - PyiCloudService, self.username, cookie_directory="./cookies" - ) - if self.auth_init_timeout_seconds > 0: - self.api = await asyncio.wait_for( - init_task, timeout=self.auth_init_timeout_seconds - ) - else: - self.api = await init_task - except Exception as e: - logger.exception("Failed to initialize iCloud session from cookies: %s", e) - return False - else: - print("No stored session. Authenticating...") - try: - init_task = asyncio.to_thread( - PyiCloudService, - self.username, - self.password, - cookie_directory="./cookies", - ) - if self.auth_init_timeout_seconds > 0: - self.api = await asyncio.wait_for( - init_task, timeout=self.auth_init_timeout_seconds - ) - else: - self.api = await init_task - except Exception as e: - logger.exception("Failed to initialize iCloud session with credentials: %s", e) - return False + logger.info( + "Initializing iCloud session from %s", + "stored cookies" if has_token else "credentials", + ) + if not await self._create_api_session(has_token=has_token): + return False if self.api.requires_2fa: - print("Two-factor authentication required.") + logger.info("Two-factor authentication required.") code = await self._request_code_from_vue("Enter the 6-digit Apple verification code") if code is None: if sys.stdin and sys.stdin.isatty(): @@ -154,29 +155,28 @@ class FindMyMonitor: "2FA required but no interactive terminal or Vue responder is available; deferring authentication" ) return False - # Verify the code result = await asyncio.to_thread(self.api.validate_2fa_code, code) - print(f"2FA validation result: {result}") + logger.info("2FA validation result: %s", result) if not result: - print("Failed to verify 2FA code") + logger.warning("Failed to verify 2FA code") return False - # Trust the session await asyncio.to_thread(self.api.trust_session) if self.api.requires_2sa: - import textwrap - print(textwrap.dedent(""" + logger.info(textwrap.dedent(""" Two-step authentication required. Please select a device to receive a SMS verification code: """)) - # List available devices for 2SA for i, device in enumerate(self.api.trusted_devices): - print( - f" {i + 1}: {device.get('deviceName', 'Unknown device')} ({device.get('phoneNumber', 'Unknown number')})") + logger.info( + " %s: %s (%s)", + i + 1, + device.get("deviceName", "Unknown device"), + device.get("phoneNumber", "Unknown number"), + ) - # Prompt the user for their choice if not (sys.stdin and sys.stdin.isatty()): logger.warning( "2SA required but no interactive terminal is available; deferring authentication" @@ -185,19 +185,18 @@ class FindMyMonitor: device_index = await asyncio.to_thread(click.prompt, "Please select a device number", type=int) - 1 device = self.api.trusted_devices[device_index] if not await asyncio.to_thread(self.api.send_verification_code, device): - print("Failed to send verification code") + logger.warning("Failed to send verification code") return False - # Prompt the user to enter the verification code they received code = await asyncio.to_thread(click.prompt, "Please enter verification code", type=int) if not await asyncio.to_thread(self.api.validate_verification_code, device, code): - print("Failed to verify verification code") + logger.warning("Failed to verify verification code") return False - print("Successfully authenticated.") + logger.info("Successfully authenticated.") return True - async def get_location(self): + async def get_location(self) -> ICloudReturnData | None: """Fetches the latest latitude and longitude.""" if not self.api: await self.authenticate() @@ -259,7 +258,7 @@ class FindMyMonitor: "deviceStatus": status['deviceStatus'], "name": status['name'] } - response = iCloudReturnData(**data) + response = ICloudReturnData(**data) return response logger.info("Location payload is None for device=%s", self.selected_device.name) return None @@ -279,14 +278,12 @@ class FindMyMonitor: self.running = False def _no_location_backoff_seconds(self, base_interval: int) -> int: - schedule = [15, 30, 60, 120, 300] - idx = min(self._no_location_streak - 1, len(schedule) - 1) - return max(base_interval, schedule[idx]) + idx = min(self._no_location_streak - 1, len(BACKOFF_SCHEDULE) - 1) + return max(base_interval, BACKOFF_SCHEDULE[idx]) def _auth_backoff_seconds(self, base_interval: int) -> int: - schedule = [15, 30, 60, 120, 300] - idx = min(self._auth_error_streak - 1, len(schedule) - 1) - return max(base_interval, schedule[idx]) + idx = min(self._auth_error_streak - 1, len(BACKOFF_SCHEDULE) - 1) + return max(base_interval, BACKOFF_SCHEDULE[idx]) async def refresh_location(self): """Fetch one location update while serializing iCloud API access.""" @@ -308,7 +305,12 @@ class FindMyMonitor: if device_data is not None: self._no_location_streak = 0 self._auth_error_streak = 0 - print(f"{device_data.timeStamp} - Location: {device_data.latitude}, {device_data.longitude}") + logger.info( + "%s - Location: %s, %s", + device_data.timeStamp, + device_data.latitude, + device_data.longitude, + ) await self.queue.put(device_data) else: self._no_location_streak += 1 diff --git a/src/pymd3_vue_location_sim/locationsimulation.py b/src/pymd3_vue_location_sim/locationsimulation.py deleted file mode 100644 index d551774..0000000 --- a/src/pymd3_vue_location_sim/locationsimulation.py +++ /dev/null @@ -1,213 +0,0 @@ -from pymobiledevice3.services.dvt.instruments.location_simulation_base import ( - LocationSimulationBase, -) -from pymobiledevice3.services.dvt.instruments.location_simulation import ( - LocationSimulation, -) - - -class LocationSimulationQueue(LocationSimulation): - def __init__(self, dvt, context: LocationSimulationState): - super().__init__(dvt) - self.context = context - - async def play_queue( - self, disable_sleep: bool = False, timing_randomness_range: int = 0 - ) -> None: - while True: - if self.context.queue_state == "PAUSED": - await asyncio.sleep(0.1) - continue - if self.context.queue_state == "SHUTDOWN": - break - loc_id = await self.context.queue.get() - if loc_id is None: - self.context.queue.task_done() - break - location_item = self.context.queue_data.get(loc_id) - if location_item is None: - logger.warning( - "Simulation queue item missing for loc_id=%s; skipping stale entry", - loc_id, - ) - self.context.queue.task_done() - continue - new_latitude = location_item.get("latitude") - new_longitude = location_item.get("longitude") - new_delay = location_item.get("delay") - new_delay = 0 if new_delay is None else new_delay - new_start = location_item.get("start") - - current_location_item = self.context.queue_data.get(self.context.loc_id) - current_latitude = ( - current_location_item.get("latitude") - if isinstance(current_location_item, dict) - else self.context.latitude - ) - current_longitude = ( - current_location_item.get("longitude") - if isinstance(current_location_item, dict) - else self.context.longitude - ) - current_start = ( - current_location_item.get("start") - if isinstance(current_location_item, dict) - else None - ) - - if self.context.set_location_enabled: - if new_delay > 0 and not disable_sleep: - countdown_delay = int(round(float(new_delay))) - if timing_randomness_range > 0: - new_delay = new_delay + random.uniform( - -timing_randomness_range, timing_randomness_range - ) - countdown_delay = int(round(float(new_delay))) - for i in range(max(0, countdown_delay), 0, -1): - self.context.next_move = i - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": current_latitude, - "longitude": current_longitude, - "start": current_start, - "next_move": i, - }, - namespace="/", - ) - await asyncio.sleep(1) - self.context.queue_data[loc_id]["start"] = self.context.queue_data[self.context.loc_id]["end"] = datetime.now(timezone.utc).isoformat() - await self.set(new_latitude, new_longitude) - self.context.loc_id = loc_id - self.context.latitude = new_latitude - self.context.longitude = new_longitude - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": self.context.latitude, - "longitude": self.context.longitude, - "start": new_start, - "next_move": None, - }, - namespace="/", - ) - logger.info( - "Set simulated location to %s, %s after %ss delay", - new_latitude, - new_longitude, - new_delay, - ) - self.context.queue.task_done() - - -class LocationSimulationTestQueue(LocationSimulationBase): - def __init__(self, context: LocationSimulationState): - super().__init__() - self.context = context - - def __enter__(self): - return self - - def __exit__(self): - return self - - async def set(self, latitude: float, longitude: float) -> None: - await asyncio.sleep(0.1) - logger.info("Simulated location set to %s, %s", latitude, longitude) - - async def clear(self) -> None: - q = self.context.queue - self.context.set_location_enabled = False - self.context.queue_state = "SHUTDOWN" - while not q.empty(): - try: - item = q.get_nowait() - q.task_done() - logger.info("Discarding item from queue: %s", item) - except asyncio.QueueEmpty: - break - await q.put(None) - - if self.context.simulation_task is not None and not self.context.simulation_task.done(): - try: - await asyncio.wait_for(self.context.simulation_task, timeout=5) - except TimeoutError: - self.context.simulation_task.cancel() - with suppress(asyncio.CancelledError): - await self.context.simulation_task - self.context.simulation_active = False - self.context.queue_state = "SHUTDOWN" - - async def play_queue( - self, disable_sleep: bool = False, timing_randomness_range: int = 0 - ) -> None: - while True: - if self.context.queue_state == "PAUSED": - await asyncio.sleep(0.1) - continue - if self.context.queue_state == "SHUTDOWN": - break - loc_id = await self.context.queue.get() - if loc_id is None: - self.context.queue.task_done() - break - location_item = self.context.queue_data.get(loc_id) - if location_item is None: - logger.warning( - "Test simulation queue item missing for loc_id=%s; skipping stale entry", - loc_id, - ) - self.context.queue.task_done() - continue - latitude = location_item.get("latitude") - longitude = location_item.get("longitude") - delay = location_item.get("delay") - delay = 0 if delay is None else delay - start_time = location_item.get("start") - if self.context.set_location_enabled: - if delay > 0 and not disable_sleep: - countdown_delay = int(round(float(delay))) - if timing_randomness_range > 0: - delay = delay + random.uniform( - -timing_randomness_range, timing_randomness_range - ) - countdown_delay = int(round(float(delay))) - for i in range(max(0, countdown_delay), 0, -1): - self.context.next_move = i - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": self.context.latitude, - "longitude": self.context.longitude, - "next_move": i, - }, - namespace="/", - ) - await asyncio.sleep(1) - await self.set(latitude, longitude) - self.context.latitude = latitude - self.context.longitude = longitude - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": self.context.latitude, - "longitude": self.context.longitude, - "next_move": None, - }, - namespace="/", - ) - logger.info( - "Set simulated location to %s, %s after %ss delay", - latitude, - longitude, - delay, - ) - self.context.queue.task_done() diff --git a/src/pymd3_vue_location_sim/models.py b/src/pymd3_vue_location_sim/models.py index 212dd1c..63488b7 100644 --- a/src/pymd3_vue_location_sim/models.py +++ b/src/pymd3_vue_location_sim/models.py @@ -1,59 +1,63 @@ -from typing import Optional, Dict +from __future__ import annotations + +from typing import Optional + from pydantic import BaseModel -class SimulationStatusData(BaseModel): + +class Coordinate(BaseModel): latitude: float longitude: float + + +class ScheduledCoordinate(Coordinate): + delay: int = 0 + start: Optional[str] = None + end: Optional[str] = None + + +class SimulationStatusData(Coordinate): start: float - end: Optional[float] - next_move: Optional[float] + end: Optional[float] = None + next_move: Optional[float] = None class SimulationStatus(BaseModel): status: bool - data: Optional[SimulationStatusData] + data: Optional[SimulationStatusData] = None -class SimulationRequestData(BaseModel): - latitude: float - longitude: float - delay: int = 0 - start: Optional[str] = None - end: Optional[str] = None +class SimulationRequestData(ScheduledCoordinate): + pass class SimulationRequest(BaseModel): status: bool - data: Optional[SimulationRequestData] + data: Optional[SimulationRequestData] = None -class SimulationRequestResponseData(BaseModel): +class SimulationRequestResponseData(ScheduledCoordinate): loc_id: str - latitude: float - longitude: float - delay: int = 0 - start: Optional[str] = None - end: Optional[str] = None + class SimulationQueueList(BaseModel): - data: Optional[SimulationRequestResponseData] + data: Optional[SimulationRequestResponseData] = None class SimulationRequestResponse(BaseModel): status: bool - data: Optional[SimulationRequestResponseData] + data: Optional[SimulationRequestResponseData] = None + class SimulationQueueDict(BaseModel): - location_id: Dict[str, SimulationRequestResponseData] + location_id: dict[str, SimulationRequestResponseData] -class iCloudLocationData(BaseModel): - latitude: float - longitude: float + +class ICloudLocationData(Coordinate): timestamp: str -class iCloudReturnData(BaseModel): - latitude: float - longitude: float + +class ICloudReturnData(Coordinate): timeStamp: int altitude: float horizontalAccuracy: float @@ -63,11 +67,16 @@ class iCloudReturnData(BaseModel): deviceStatus: int name: str -class LatLng(BaseModel): - latitude: float - longitude: float + +class LatLng(Coordinate): + pass + class ORSRequest(BaseModel): geometry_simplify: bool - coordinates: List[List[float]] + coordinates: list[list[float]] + +# Backward compatibility aliases for existing imports. +iCloudLocationData = ICloudLocationData +iCloudReturnData = ICloudReturnData diff --git a/src/pymd3_vue_location_sim/routes/api.py b/src/pymd3_vue_location_sim/routes/api.py deleted file mode 100644 index 718aac5..0000000 --- a/src/pymd3_vue_location_sim/routes/api.py +++ /dev/null @@ -1,379 +0,0 @@ -""" Tunnel Functions""" - - @self._app.get("/start-tunnel") - async def start_tunnel( - udid: Optional[str] = self.context.udid, - ip: Optional[str] = None, - connection_type: Optional[str] = None, - ) -> fastapi.Response: - udid_tunnels = [ - t.tunnel - for t in self._tunneld_core.tunnel_tasks.values() - if t.udid == udid and t.tunnel is not None - ] - if len(udid_tunnels) > 0: - self.context.udid = udid - data = { - "interface": udid_tunnels[0].interface, - "port": udid_tunnels[0].port, - "address": udid_tunnels[0].address, - } - return generate_http_response(data) - queue = asyncio.Queue() - created_task = False - try: - if not created_task and connection_type in ("usbmux", None): - task_identifier = f"usbmux-{udid}" - try: - async with await create_using_usbmux(udid) as lockdown: - service = await CoreDeviceTunnelProxy.create(lockdown) - task = asyncio.create_task( - self._tunneld_core.start_tunnel_task( - task_identifier, - service, - protocol=TunnelProtocol.TCP, - queue=queue, - ), - name=f"start-tunnel-task-{task_identifier}", - ) - self._tunneld_core.tunnel_tasks[task_identifier] = TunnelTask( - task=task, udid=udid - ) - created_task = True - except ConnectionFailedError, InvalidServiceError, MuxException: - pass - if connection_type in ("usb", None): - for rsd in await get_rsds(udid=udid): - rsd_ip = rsd.service.address[0] - if ip is not None and rsd_ip != ip: - await rsd.close() - continue - task = asyncio.create_task( - self._tunneld_core.start_tunnel_task( - rsd_ip, - await create_core_device_tunnel_service_using_rsd(rsd), - queue=queue, - ), - name=f"start-tunnel-usb-{rsd_ip}", - ) - self._tunneld_core.tunnel_tasks[rsd_ip] = TunnelTask( - task=task, udid=rsd.udid - ) - created_task = True - if not created_task and connection_type in ("wifi", None): - for remotepairing in await get_remote_pairing_tunnel_services( - udid=udid - ): - remotepairing_ip = remotepairing.hostname - if ip is not None and remotepairing_ip != ip: - await remotepairing.close() - continue - task = asyncio.create_task( - self._tunneld_core.start_tunnel_task( - remotepairing_ip, remotepairing, queue=queue - ), - name=f"start-tunnel-wifi-{remotepairing_ip}", - ) - self._tunneld_core.tunnel_tasks[remotepairing_ip] = TunnelTask( - task=task, udid=remotepairing.remote_identifier - ) - created_task = True - except Exception as e: - return fastapi.Response( - status_code=501, - content=json.dumps( - { - "error": { - "exception": e.__class__.__name__, - "traceback": traceback.format_exc(), - } - } - ), - ) - if not created_task: - return fastapi.Response( - status_code=501, content=json.dumps({"error": "task not created"}) - ) - tunnel: Optional[TunnelResult] = await queue.get() - if tunnel is not None: - self.context.udid = udid - data = { - "interface": tunnel.interface, - "port": tunnel.port, - "address": tunnel.address, - } - return generate_http_response(data) - else: - return fastapi.Response( - status_code=404, - content=json.dumps( - {"error": "something went wrong during tunnel creation"} - ), - ) - - @self._app.get("/restart-tunneld") - async def restart() -> fastapi.Response: - """Restart Tunneld""" - self._tunneld_core.clear() - await asyncio.sleep(2) - self._tunneld_core.start() - data = { - "operation": "restart-tunneld", - "data": True, - "message": "Restarting tunneld...", - } - return generate_http_response(data) - - - @self._app.get("/shutdown") - async def shutdown() -> fastapi.Response: - """Shutdown Tunneld""" - os.kill(os.getpid(), signal.SIGINT) - data = { - "operation": "shutdown", - "data": True, - "message": "Server shutting down...", - } - return generate_http_response(data) - - @self._app.get("/clear-tunnels") - async def clear_tunnels() -> fastapi.Response: - """Clear all tunnels""" - self._tunneld_core.clear() - data = { - "operation": "clear_tunnels", - "data": True, - "message": "Cleared tunnels...", - } - return generate_http_response(data) - - @self._app.get("/cancel") - async def cancel_tunnel(udid: str) -> fastapi.Response: - """Cancel a tunnel""" - self._tunneld_core.cancel(udid=udid) - data = { - "operation": "cancel", - "udid": udid, - "data": True, - "message": f"tunnel {udid} Canceled ...", - } - return generate_http_response(data) - - """Simulation Functions""" - - @self._app.get("/start-simulation") - async def app_start_simulation() -> fastapi.Response: - logger.info("Simulation Start Requested ") - if ( - self.context.simulation_task is None - or self.context.simulation_task.done() - ): - await start_icloud_monitor() - self.context.simulation_active = True - self.context.simulation_task = asyncio.create_task( - start_simulation_queue(), - name="location-simulation-worker", - ) - data = {"status": "started", "message": "Simulation started"} - else: - data = {"status": "error", "message": "Simulation already running"} - return generate_http_response(data) - - @self._app.get("/start-icloud-monitor") - async def app_start_icloud_monitor() -> fastapi.Response: - await start_icloud_monitor() - data = { - "status": "started", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - return generate_http_response(data) - - @self._app.get("/stop-icloud-monitor") - async def app_stop_icloud_monitor() -> fastapi.Response: - await end_icloud_monitor() - data = { - "status": "stopped", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - return generate_http_response(data) - - @self._app.get("/icloud-monitor-status") - async def app_icloud_monitor_status() -> fastapi.Response: - data = { - "status": "ok", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - return generate_http_response(data) - - @self._app.post("/add-location") - async def app_add_location(data: SimulationRequestData) -> fastapi.Response: - """Add a location to the simulation queue""" - logger.info("Request to add new location to queue") - - loc_id = str(uuid.uuid4()) - latitude = ( - data.get("latitude") - if isinstance(data, dict) - else getattr(data, "latitude", None) - ) - longitude = ( - data.get("longitude") - if isinstance(data, dict) - else getattr(data, "longitude", None) - ) - delay = ( - data.get("delay", 0) - if isinstance(data, dict) - else getattr(data, "delay", 0) - ) - try: - delay = parse_delay_seconds(delay) - except ValueError as e: - return generate_http_response( - {"status": "error", "message": str(e)}, - status_code=400, - ) - if latitude is not None and longitude is not None: - logger.info( - "Adding location %s (%s, %s) with %s delay to the queue", - loc_id, - latitude, - longitude, - delay, - ) - accrued_delay = 0 - if self.context.queue_data: - accrued_delay = sum( - parse_delay_seconds(item.get("delay", 0)) - for item in self.context.queue_data.values() - ) - now_time = datetime.now(timezone.utc) - new_time = ( - now_time - + timedelta(seconds=accrued_delay) - + timedelta(seconds=delay) - ) - start_time = new_time.isoformat() - location_item = { - "loc_id": loc_id, - "latitude": latitude, - "longitude": longitude, - "delay": delay, - "start": start_time, - "status": "queued", - } - resp = { - "status": "added", - "message": f"Location {loc_id} added to the queue", - "item": location_item, - } - await self.context.queue.put(loc_id) - add_item(loc_id, location_item) - logger.info("Location %s added to the queue", loc_id) - else: - resp = {"status": "error", "message": "Invalid location data"} - return generate_http_response(resp) - - @self._app.get("/clear-queue") - async def app_clear_queue() -> fastapi.Response: - """Clear the simulation queue""" - logger.info("Simulation Start Requested ") - await empty_simulation_queue() - data = {"status": "cleared", "message": "Simulation cleared"} - return generate_http_response(data) - - @self._app.get("/pause-queue") - async def app_pause_queue() -> fastapi.Response: - """Pause the simulation queue""" - await pause_simulation_queue() - data = {"status": "paused", "message": "Simulation paused"} - return generate_http_response(data) - - @self._app.get("/resume-queue") - async def app_resume_queue() -> fastapi.Response: - """Resume the simulation queue""" - await resume_simulation_queue() - data = {"status": "resumed", "message": "Simulation resumed"} - return generate_http_response(data) - - @self._app.get("/end-simulation") - async def app_end_simulation() -> fastapi.Response: - """End the simulation queue""" - logger.info("End location simulation request") - end_task = asyncio.create_task( - end_simulation_queue(), name="end-simulation-worker" - ) - result = await end_task - data = {"status": result, "message": "Simulation ended"} - return generate_http_response(data) - - """Status Functions""" - - @self._app.get("/") - async def list_tunnels() -> dict[str, list[dict]]: - """Retrieve the available tunnels and format them as {UUID: TUNNEL_ADDRESS}""" - tunnels = {} - for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items(): - if (active_tunnel.udid is None) or (active_tunnel.tunnel is None): - continue - if active_tunnel.udid not in tunnels: - tunnels[active_tunnel.udid] = [] - tunnels[active_tunnel.udid].append( - { - "tunnel-address": active_tunnel.tunnel.address, - "tunnel-port": active_tunnel.tunnel.port, - "interface": ip, - } - ) - return tunnels - - - @self._app.get("/device-info") - async def device_info(): - """Get device information""" - tunnels = {} - for ip, active_tunnel in self._tunneld_core.tunnel_tasks.items(): - if (active_tunnel.udid is None) or (active_tunnel.tunnel is None): - continue - if active_tunnel.udid not in tunnels: - tunnels[active_tunnel.udid] = {} - try: - lockdown = await create_using_usbmux( - serial=active_tunnel.udid, autopair=False - ) - tunnels[active_tunnel.udid] = iterate_multidim(lockdown.all_values) - except Exception as e: - logger.error( - f"Failed to create lockdown session for device {active_tunnel.udid}: {e}" - ) - continue - return tunnels - - @self._app.get("/device-name") - async def rsd_info(): - """Get rsd information""" - device_name = await get_device_name() - return generate_http_response(device_name) - - @self._app.get("/rsd-info") - async def rsd_info(): - """Get rsd information""" - rsd_info = {} - if self.context.tunnel is None: - await get_tun() - if self.context.tunnel is not None: - rsd_info = self.context.tunnel.peer_info - return generate_http_response(rsd_info) - - @self._app.get("/hello") - async def hello() -> fastapi.Response: - data = {"message": "Hello, I'm alive"} - return generate_http_response(data) - - @self._app.get("/context-status") - async def app_context_status() -> fastapi.Response: - data = get_status() - return generate_http_response(data) diff --git a/src/pymd3_vue_location_sim/routes/socketio.py b/src/pymd3_vue_location_sim/routes/socketio.py deleted file mode 100644 index 203757b..0000000 --- a/src/pymd3_vue_location_sim/routes/socketio.py +++ /dev/null @@ -1,425 +0,0 @@ -""" Socket.IO Functions""" - - -async def sio_send_status(sid): - """Send Current Status""" - await self.context.sio.emit( - "status", jsonable_encoder(get_status()), namespace="/", to=sid - ) - - -"""Socket.IO Connection Events""" - - -@self.context.sio.event -async def connect(sid, environ): - """Client connection event handler.""" - self.context.connected_clients.add(sid) - logger.info("Client connected: %s", sid) - await sio_send_status(sid) - return "%s connected" % sid - - -@self.context.sio.event -async def disconnect(sid): - """Client disconnection event handler.""" - self.context.connected_clients.discard(sid) - logger.info("Client disconnected: %s", sid) - - -""" Socket.IO Request Events """ - - -@self.context.sio.event -async def request_update(sid): - status_update = jsonable_encoder(get_status()) - logger.info("Update request from %s sending %s", sid, status_update) - return status_update - - -@self.context.sio.event -async def message(sid, data): - logger.info("Received message from %s: %s", sid, data) - return True, "Message received" - - -# await self.context.sio.emit("message", f"Received message from {sid}: {data}", namespace="/") - -""" Device Control""" - - -@self.context.sio.event -async def device_control(sid, data): - """Device Control""" - command = ( - data.get("command") - if isinstance(data, dict) - else getattr(data, "command", None) - ) - delay = ( - data.get("delay") - if isinstance(data, dict) - else getattr(data, "delay", None) - ) - if delay is None: - delay = 5 - match command: - case "shutdown": - """ Shutdown the device""" - logger.info( - "Shutdown command received from %s with delay %s", sid, delay - ) - await device_shutdown(delay) - return { - "command": "shutdown", - "status": "success", - "message": f"Device shutdown initiated with {delay} seconds delay", - } - - case "reboot": - """ Reboot the device""" - logger.info( - "Reboot command received from %s with delay %s", sid, delay - ) - await device_reboot(delay) - return { - "command": "reboot", - "status": "success", - "message": f"Device reboot initiated with {delay} seconds delay", - } - - case _: - return { - "command": command, - "status": "error", - "message": f"Invalid command: {command}", - } - - -@self.context.sio.event -async def simulation_control(sid, data): - """Simulation Control""" - command = ( - data.get("command") - if isinstance(data, dict) - else getattr(data, "command", None) - ) - logger.info( - "Simulation Control command: %s requested from %s", command, sid - ) - try: - match command: - case "add": - """ Add a location to the simulation queue""" - loc_id = str(uuid.uuid4()) - latitude = ( - data.get("latitude") - if isinstance(data, dict) - else getattr(data, "latitude", None) - ) - longitude = ( - data.get("longitude") - if isinstance(data, dict) - else getattr(data, "longitude", None) - ) - delay = ( - data.get("delay", 0) - if isinstance(data, dict) - else getattr(data, "delay", 0) - ) - try: - delay = parse_delay_seconds(delay) - except ValueError as e: - return { - "command": command, - "status": "error", - "message": str(e), - } - if latitude is not None and longitude is not None: - logger.info( - "Adding location %s (%s, %s) with %s delay to the queue", - loc_id, - latitude, - longitude, - delay, - ) - accrued_delay = 0 - if self.context.queue_data: - accrued_delay = sum( - parse_delay_seconds(item.get("delay", 0)) - for item in self.context.queue_data.values() - ) - now_time = datetime.now(timezone.utc) - new_time = ( - now_time - + timedelta(seconds=accrued_delay) - + timedelta(seconds=delay) - ) - start_time = new_time.isoformat() - coords = f"{latitude}, {longitude}" - rev_geocode = self.context.reverse_geocode(coords) - if rev_geocode: - address = rev_geocode.address - else: - address = f"{latitude}, {longitude}" - - location_item = { - "loc_id": loc_id, - "latitude": latitude, - "longitude": longitude, - "delay": delay, - "start": start_time, - "address": address, - } - ack = { - "command": command, - "status": "added", - "message": f"Location {loc_id} added to the queue", - "item": location_item, - } - await self.context.queue.put(loc_id) - add_item(loc_id, location_item) - logger.info("Location %s added to the queue", loc_id) - return ack - else: - logger.warning( - "Invalid location data received from %s: %s", sid, data - ) - return { - "command": command, - "status": "error", - "message": "Invalid location data", - "data": location_item, - } - case "clear": - """ Clear the simulation queue""" - await empty_simulation_queue() - return { - "command": command, - "status": "cleared", - "message": "Simulation cleared", - } - case "pause": - """ Pause the simulation queue""" - await pause_simulation_queue() - return { - "command": command, - "status": "paused", - "message": "Simulation paused", - } - case "resume": - """ Resume the simulation queue""" - await resume_simulation_queue() - return { - "command": command, - "status": "resumed", - "message": "Simulation resumed", - } - case "end": - """ End the simulation queue""" - logger.info("End location simulation request from %s", sid) - end_task = asyncio.create_task( - end_simulation_queue(), name="end-simulation-worker" - ) - result = await end_task - simstatus = not result - return { - "command": command, - "status": simstatus, - "message": "Simulation ended", - } - case "start": - """ Start the simulation queue""" - logger.info("Start location simulation request from %s", sid) - if ( - self.context.simulation_task is None - or self.context.simulation_task.done() - ): - await start_icloud_monitor() - self.context.simulation_active = True - self.context.queue_state = "RUNNING" - self.context.simulation_task = asyncio.create_task( - start_simulation_queue(), - name="location-simulation-worker", - ) - return { - "command": command, - "status": self.context.queue_state, - "message": "Simulation started", - } - else: - return { - "command": command, - "status": "error", - "message": "Simulation already running", - } - case _: - logger.warning( - "Invalid command received from %s: %s", sid, command - ) - return {"status": "error", "message": "Invalid command"} - finally: - await sio_send_status(sid) - - -@self.context.sio.event -async def icloud_monitor_control(sid, data): - command = ( - data.get("command") - if isinstance(data, dict) - else getattr(data, "command", None) - ) - logger.info( - "iCloud Monitor control command: %s requested from %s", command, sid - ) - try: - match command: - case "start": - await start_icloud_monitor() - return { - "command": command, - "status": "running", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - case "stop": - await end_icloud_monitor() - return { - "command": command, - "status": "stopped", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - case "status": - return { - "command": command, - "status": "ok", - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - case _: - return { - "command": command, - "status": "error", - "message": "Invalid command", - } - finally: - await sio_send_status(sid) - - -""" Tunnel Control """ - - -@self.context.sio.event -async def tunneld_control(sid, data): - command = ( - data.get("command") - if isinstance(data, dict) - else getattr(data, "command", None) - ) - logger.info("Tunneld Control command: %s requested from %s", command, sid) - match command: - case "start": - """Start Tunneld""" - logger.info("Start tunneld request from %s: %s", sid, data) - try: - self._tunneld_core.start() - logger.info("Tunneld started successfully") - return { - "status": "running", - "message": "Tunneld started successfully", - } - except Exception as e: - logger.error("Error starting tunneld: %s", e) - return { - "command": command, - "status": "error", - "message": f"Error starting tunneld: {e}", - } - - case "start-watcher": - """ Start Tunneld Watcher """ - logger.info("Start tunneld watcher request from %s: %s", sid, data) - await start_tunnel_watcher() - return { - "status": "running", - "message": "Tunneld watcher started successfully", - } - - case "end-watcher": - """ End Tunneld Watcher """ - logger.info("End tunneld watcher request from %s: %s", sid, data) - await end_tunnel_watcher() - return { - "status": "stopped", - "message": "Tunneld watcher stopped successfully", - } - - case "shutdown": - """Shutdown Tunneld""" - logger.info("Shutdown tunneld request from %s: %s", sid, data) - try: - os.kill(os.getpid(), signal.SIGINT) - return { - "command": command, - "status": "Success", - "message": "Server shutting down...", - } - except Exception as e: - logger.error("Error shutting down tunneld: %s", e) - return { - "command": command, - "status": "error", - "message": f"Error shutting down tunneld: {e}", - } - - case "clear": - """Clear all tunnels""" - logger.info("Clearing tunnels...") - try: - self._tunneld_core.clear() - return { - "command": command, - "status": "Success", - "message": "Cleared tunnels...", - } - except Exception as e: - logger.error("Error clearing tunnels: %s", e) - return { - "command": command, - "status": "error", - "message": f"Error clearing tunnels: {e}", - } - - case "cancel": - """Cancel a tunnel""" - logger.info("Canceling tunnel request from %s: %s", sid, data) - try: - udid = ( - data.get("udid") - if isinstance(data, dict) - else getattr(data, "udid", self.context.udid) - ) - if udid is None: - udid = self.context.udid - self._tunneld_core.cancel(udid=udid) - return { - "command": command, - "status": "Success", - "udid": udid, - "message": f"tunnel {udid} Canceled ...", - } - except Exception as e: - logger.error("Error canceling tunnel: %s", e) - return { - "command": command, - "status": "error", - "message": f"Error canceling tunnel: {e}", - } - - case _: - return { - "command": command, - "status": "error", - "message": f"Unknown operation: {command}", - } \ No newline at end of file diff --git a/src/pymd3_vue_location_sim/server.py b/src/pymd3_vue_location_sim/server.py index 3ce24f0..81f6b05 100644 --- a/src/pymd3_vue_location_sim/server.py +++ b/src/pymd3_vue_location_sim/server.py @@ -14,6 +14,7 @@ import httpx from contextlib import suppress from typing import Optional, Dict from dotenv import load_dotenv +from sqlalchemy.util import await_only with warnings.catch_warnings(): # Ignore: "Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater." @@ -64,7 +65,6 @@ from .models import ( SimulationRequestResponseData, iCloudLocationData, LatLng, - ORSRequest ) from .json_formatter import JsonFormatter, handler, root_logger, logger from .geo_cache import AsyncReverseGeocoder @@ -95,11 +95,14 @@ TUNNEL_ACQUIRE_TIMEOUT_SECONDS = 15 DVT_CONNECT_TIMEOUT_SECONDS = 20 +def env_flag(name: str, default: str = "True") -> bool: + return os.getenv(name, default).lower() == "true" + + class LocationSimulationState: def __init__(self): self.connected_clients: set[str] = set() - self.current_location: Optional[Dict[str, - SimulationRequestResponseData]] = None + self.current_location: Optional[Dict[str, str]] = None self.device_name: Optional[str] = os.getenv("SELECTED_DEVICE_NAME") self.fmf_location: Optional[iCloudLocationData] = None self.fmf_queue: asyncio.Queue = asyncio.Queue() @@ -108,13 +111,11 @@ class LocationSimulationState: self.icloud_monitor_task = None self.icloud_monitor_enabled: bool = False self.tunnel_watcher_task = None - self.latitude: Optional[float] = None - self.loc_id: Optional[str] = None - self.longitude: Optional[float] = None self.next_move: Optional[float] = None self.simulation_queue: asyncio.Queue = asyncio.Queue() self.simulation_queue_data: Dict = {} self.simulation_queue_order: list[str] = [] + self.simulation_queue_pending_ids: set[str] = set() self.simulation_queue_state: str = "STOPPED" self.simulation_noise: bool = False self.set_location_enabled: bool = True @@ -123,11 +124,34 @@ class LocationSimulationState: self.sio: socketio.AsyncServer = socketio.AsyncServer( async_mode="asgi", cors_allowed_origins="*" ) - self.test_mode: bool = os.getenv("TEST_MODE", "True").lower() == "true" + self.test_mode: bool = env_flag("TEST_MODE", "True") self.tunnel: Optional[RemoteServiceDiscoveryService] = None self.udid: Optional[str] = None self.reverse_geocode = AsyncReverseGeocoder() + def get_current_loc_id(self) -> Optional[str]: + if not isinstance(self.current_location, dict): + return None + loc_id = self.current_location.get("loc_id") + if isinstance(loc_id, str) and loc_id: + return loc_id + return None + + def set_current_loc_id(self, loc_id: Optional[str]) -> None: + if isinstance(loc_id, str) and loc_id: + self.current_location = {"loc_id": loc_id} + else: + self.current_location = None + + def get_current_item(self) -> Optional[dict]: + current_loc_id = self.get_current_loc_id() + if current_loc_id is None: + return None + item = self.simulation_queue_data.get(current_loc_id) + if isinstance(item, dict): + return item + return None + class TunneldRunnerSio: """TunneldRunner orchestrate between the webserver and TunneldCore""" @@ -137,7 +161,7 @@ class TunneldRunnerSio: cls, host: str, port: int, - context: LocationSimulationState = LocationSimulationState(), + context: Optional[LocationSimulationState] = None, protocol: TunnelProtocol = TunnelProtocol.QUIC, usb_monitor: bool = True, wifi_monitor: bool = True, @@ -159,7 +183,7 @@ class TunneldRunnerSio: self, host: str, port: int, - context: LocationSimulationState = LocationSimulationState(), + context: Optional[LocationSimulationState] = None, protocol: TunnelProtocol = TunnelProtocol.QUIC, usb_monitor: bool = True, wifi_monitor: bool = True, @@ -167,7 +191,8 @@ class TunneldRunnerSio: mobdev2_monitor: bool = True, ): async def app_startup() -> None: - logger.info("Application startup: starting tunneld core and watcher") + logger.info( + "Application startup: starting tunneld core and watcher") self._tunneld_core.start() await start_tunnel_watcher() @@ -180,9 +205,9 @@ class TunneldRunnerSio: await self.context.sio.shutdown() self.host = host - self.port = port + self.port = os.getenv("PORT") self.protocol = protocol - self.context = context + self.context = context or LocationSimulationState() self._tunneld_api_address = ( "127.0.0.1" if host in ("0.0.0.0", "::") else host, port, @@ -211,6 +236,8 @@ class TunneldRunnerSio: mobdev2_monitor=mobdev2_monitor, ) + """Tunnel Functions""" + async def get_tun( udid: Optional[str] = None, max_retries: int = 10, retry_delay: float = 0.5 ) -> RemoteServiceDiscoveryService: @@ -275,12 +302,6 @@ class TunneldRunnerSio: raise TunneldConnectionError() - async def get_device_name(): - if self.context.tunnel is None: - await get_tun() - device_name = await self.context.tunnel.get_value(key='DeviceName') - return device_name - def collect_active_tunnels() -> Dict[str, dict]: active: Dict[str, dict] = {} for interface, tunnel_task in self._tunneld_core.tunnel_tasks.items(): @@ -296,14 +317,22 @@ class TunneldRunnerSio: } return active + """Tunnel Watcher""" + async def handle_tunnel_drop(disconnected: list[dict]) -> None: - disconnected_udids = {d.get("udid") - for d in disconnected if d.get("udid")} + disconnected_udids: set[str] = set() + for item in disconnected: + udid = item.get("udid") + if isinstance(udid, str) and udid: + disconnected_udids.add(udid) if not disconnected_udids: return current_active = collect_active_tunnels() - active_udids = {d.get("udid") - for d in current_active.values() if d.get("udid")} + active_udids: set[str] = set() + for item in current_active.values(): + udid = item.get("udid") + if isinstance(udid, str) and udid: + active_udids.add(udid) primary_disconnected = self.context.udid in disconnected_udids if self.context.udid else False no_tunnels_left = len(current_active) == 0 if not primary_disconnected and not no_tunnels_left: @@ -333,17 +362,6 @@ class TunneldRunnerSio: ) await end_icloud_monitor() - async def safe_sio_emit(event: str, payload: dict, timeout_seconds: float = 2.0) -> None: - try: - await asyncio.wait_for( - self.context.sio.emit(event, payload, namespace="/"), - timeout=timeout_seconds, - ) - except TimeoutError: - logger.warning("Socket.IO emit timed out for event=%s", event) - except Exception: - logger.exception("Socket.IO emit failed for event=%s", event) - async def tunnel_watcher_loop() -> None: previous = collect_active_tunnels() while True: @@ -373,7 +391,8 @@ class TunneldRunnerSio: selected_device_name = self.context.device_name if selected_device_name and device_name != selected_device_name: wrong_udid = self.context.udid - self._tunneld_core.cancel(udid=item.get("udid")) + self._tunneld_core.cancel( + udid=item.get("udid")) logger.warning( "Tunnel established to wrong device. Dropping tunnel. wrong_udid=%s for device: %s", wrong_udid, selected_device_name) @@ -424,7 +443,8 @@ class TunneldRunnerSio: with suppress(Exception): exc = self.context.tunnel_watcher_task.exception() if exc is not None: - logger.error("Previous tunnel watcher task exited with error: %s", exc) + logger.error( + "Previous tunnel watcher task exited with error: %s", exc) self.context.tunnel_watcher_task = asyncio.create_task( tunnel_watcher_loop(), name="tunnel-task-watcher", @@ -437,29 +457,173 @@ class TunneldRunnerSio: await self.context.tunnel_watcher_task self.context.tunnel_watcher_task = None - def cleanup_device_data(d): + """Helper Functions""" + + def parse_data(d): mydict = {} for key, value in d.items(): if isinstance(value, dict): - cleanup_device_data(value) + parse_data(value) + elif ( + isinstance(value, str) + or isinstance(value, int) + or isinstance(value, float) + or isinstance(value, bool) + or isinstance(value, list) + ): + mydict[key] = value elif isinstance(value, bytes): - mydict[key] = "BYTE DATA" + mydict[key] = 'BYTE DATA' else: + mydict[key] = "" mydict[key] = value return mydict + async def get_device_name(): + if self.context.tunnel is None: + await get_tun() + device_name = await self.context.tunnel.get_value(key='DeviceName') + return device_name + + async def safe_sio_emit(event: str, payload: dict, timeout_seconds: float = 2.0) -> None: + try: + await asyncio.wait_for( + self.context.sio.emit(event, payload, namespace="/"), + timeout=timeout_seconds, + ) + except TimeoutError: + logger.warning("Socket.IO emit timed out for event=%s", event) + except Exception: + logger.exception("Socket.IO emit failed for event=%s", event) + + def parse_delay_seconds(raw_delay) -> int: + if raw_delay is None: + return 0 + if isinstance(raw_delay, bool): + raise ValueError("delay must be a non-negative number") + parsed = float(raw_delay) + if math.isnan(parsed) or math.isinf(parsed) or parsed < 0: + raise ValueError("delay must be a non-negative finite number") + return int(parsed) + + """Host power down functions""" + async def device_reboot(delay): """Reboot the device""" - logger.info("Reboot iniated with delay %s") + logger.info("Reboot initiated with delay %s") await asyncio.sleep(delay) os.system("shutdown -r now") async def device_shutdown(delay): """Shutdown the device""" - logger.info("Shutdown iniated with delay %s") + logger.info("Shutdown initiated with delay %s") await asyncio.sleep(delay) os.system("shutdown -h now") + """iCloud Find My Monitor""" + + async def start_icloud_monitor(): + """Start Apple iCloud Find My Monitor to retrieve actual reported device location""" + logger.info("iCloud monitor start requested") + self.context.icloud_monitor_enabled = True + if ( + self.context.icloud_monitor_task is None + or self.context.icloud_monitor_task.done() + ): + self.context.icloud_monitor_task = asyncio.create_task( + self.context.icloud_monitor.run_monitor(interval=30), + name="icloud-monitor-producer", + ) + logger.info("iCloud monitor producer task started") + if ( + self.context.icloud_consumer_task is None + or self.context.icloud_consumer_task.done() + ): + self.context.icloud_consumer_task = asyncio.create_task( + consume_icloud_updates(), + name="icloud-monitor-consumer", + ) + logger.info("iCloud monitor consumer task started") + + async def consume_icloud_updates(): + while True: + updated_location = await self.context.fmf_queue.get() + try: + if self.context.fmf_location != updated_location: + self.context.fmf_location = updated_location + await self.context.sio.emit( + "fmf_update", updated_location.model_dump(), namespace="/" + ) + finally: + self.context.fmf_queue.task_done() + + async def refresh_icloud_location() -> dict: + """Fetch one iCloud location update, with or without monitor loop running.""" + logger.info("iCloud monitor refresh requested") + try: + updated_location = await self.context.icloud_monitor.refresh_location() + except Exception as e: + logger.exception("Failed to refresh iCloud location: %s", e) + return { + "command_status": "ERROR", + "command_class": "icloud_monitor", + "command": "refresh", + "message": "Failed to refresh iCloud location", + "error": type(e).__name__, + "icloud_monitor_enabled": self.context.icloud_monitor_enabled, + "icloud_monitor_running": is_icloud_monitor_running(), + } + + if updated_location is None: + return { + "command_status": "OK", + "command_class": "icloud_monitor", + "command": "refresh", + "location_found": False, + "location_updated": False, + "icloud_monitor_enabled": self.context.icloud_monitor_enabled, + "icloud_monitor_running": is_icloud_monitor_running(), + } + + location_updated = self.context.fmf_location != updated_location + self.context.fmf_location = updated_location + await self.context.sio.emit( + "fmf_update", updated_location.model_dump(), namespace="/" + ) + return { + "command_status": "OK", + "command_class": "icloud_monitor", + "command": "refresh", + "location_found": True, + "location_updated": location_updated, + "icloud_monitor_enabled": self.context.icloud_monitor_enabled, + "icloud_monitor_running": is_icloud_monitor_running(), + "fmf_location": updated_location, + } + + async def end_icloud_monitor(): + logger.info("iCloud monitor stop requested") + self.context.icloud_monitor_enabled = False + self.context.icloud_monitor.stop() + if self.context.icloud_monitor_task is not None: + self.context.icloud_monitor_task.cancel() + with suppress(asyncio.CancelledError): + await self.context.icloud_monitor_task + self.context.icloud_monitor_task = None + if self.context.icloud_consumer_task is not None: + self.context.icloud_consumer_task.cancel() + with suppress(asyncio.CancelledError): + await self.context.icloud_consumer_task + self.context.icloud_consumer_task = None + + def is_icloud_monitor_running() -> bool: + return ( + self.context.icloud_monitor_task is not None + and not self.context.icloud_monitor_task.done() + and self.context.icloud_consumer_task is not None + and not self.context.icloud_consumer_task.done() + ) + """ Queue Functions""" async def start_simulation_queue(): @@ -473,10 +637,27 @@ class TunneldRunnerSio: start_queue_worker(), name="location-simulation-worker", ) - data = {"status": "started", "message": "Simulation started"} + data = { + "command_status": "OK", + "command_class": "simulation_control", + "command": "start", + "message": "Simulation started", + "data": { + "simulation_active": self.context.simulation_active, + "simulation_queue_state": self.context.simulation_queue_state + } + } else: - data = {"status": "error", - "message": "Simulation already running"} + data = { + "command_status": "ERROR", + "command_class": "simulation_control", + "command": "start", + "message": "Simulation already running", + "data": { + "simulation_active": self.context.simulation_active, + "simulation_queue_state": self.context.simulation_queue_state + } + } return data async def start_queue_worker(): @@ -607,10 +788,15 @@ class TunneldRunnerSio: if isinstance(data, dict) else getattr(data, "delay", 0) ) + address = ( + data.get("address") + if isinstance(data, dict) + else getattr(data, "address", None) + ) try: delay = parse_delay_seconds(delay) except ValueError as e: - return {"status": "error", "command": "add", "message": str(e)} + return {"command_status": "ERROR", "command_class": "simulation_control", "command": "add", "message": str(e)} if latitude is not None and longitude is not None: logger.info( "Adding location %s (%s, %s) with %s delay to the queue", @@ -620,9 +806,10 @@ class TunneldRunnerSio: delay, ) accrued_delay = 0 + current_loc_id = self.context.get_current_loc_id() if self.context.simulation_queue_data and len(self.context.simulation_queue_order) > 1: current_index = get_item_index( - self.context.loc_id) if self.context.loc_id else 0 + current_loc_id) if current_loc_id else 0 remaining_items = self.context.simulation_queue_order[current_index + 1:] accrued_delay = sum(parse_delay_seconds( self.context.simulation_queue_data[loc_id]['delay']) for loc_id in remaining_items if loc_id in self.context.simulation_queue_data) @@ -636,130 +823,38 @@ class TunneldRunnerSio: + timedelta(seconds=delay) ) start_time = new_time.isoformat() - rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude) - if rev_geocode: - address = rev_geocode - else: - address = f"{latitude}, {longitude}" +# rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude) +# if rev_geocode: +# address = rev_geocode +# else: +# address = f"{latitude}, {longitude}" location_item = { "loc_id": loc_id, "latitude": latitude, "longitude": longitude, "delay": delay, + "status": "queued", "start": start_time, "address": address, } resp = { - "status": "OK", + "command_status": "OK", + "command_class": "simulation_control", "command": "add", "message": f"Location {loc_id} added to the queue", - "item": location_item, + "data": location_item, } - await self.context.simulation_queue.put(loc_id) add_item(loc_id, location_item) + await enqueue_next_simulation_item() logger.info("Location %s added to the queue", loc_id) else: logger.error("Invalid location data: %s", data) - resp = {"status": "error", + resp = {"command_status": "ERROR", + "command_class": "simulation_control", + "command": "add", "message": "Invalid location data", "data": data} return resp - async def start_icloud_monitor(): - """Start Apple iCloud Find My Monitor to retreive actual reported device location""" - logger.info("iCloud monitor start requested") - self.context.icloud_monitor_enabled = True - if ( - self.context.icloud_monitor_task is None - or self.context.icloud_monitor_task.done() - ): - self.context.icloud_monitor_task = asyncio.create_task( - self.context.icloud_monitor.run_monitor(interval=30), - name="icloud-monitor-producer", - ) - logger.info("iCloud monitor producer task started") - if ( - self.context.icloud_consumer_task is None - or self.context.icloud_consumer_task.done() - ): - self.context.icloud_consumer_task = asyncio.create_task( - consume_icloud_updates(), - name="icloud-monitor-consumer", - ) - logger.info("iCloud monitor consumer task started") - - async def consume_icloud_updates(): - while True: - updated_location = await self.context.fmf_queue.get() - try: - if self.context.fmf_location != updated_location: - self.context.fmf_location = updated_location - await self.context.sio.emit( - "fmf_update", updated_location.model_dump(), namespace="/" - ) - finally: - self.context.fmf_queue.task_done() - - async def refresh_icloud_location() -> dict: - """Fetch one iCloud location update, with or without monitor loop running.""" - logger.info("iCloud monitor refresh requested") - try: - updated_location = await self.context.icloud_monitor.refresh_location() - except Exception as e: - logger.exception("Failed to refresh iCloud location: %s", e) - return { - "status": "error", - "message": "Failed to refresh iCloud location", - "error": type(e).__name__, - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - - if updated_location is None: - return { - "status": "ok", - "location_found": False, - "location_updated": False, - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - } - - location_updated = self.context.fmf_location != updated_location - self.context.fmf_location = updated_location - await self.context.sio.emit( - "fmf_update", updated_location.model_dump(), namespace="/" - ) - return { - "status": "ok", - "location_found": True, - "location_updated": location_updated, - "icloud_monitor_enabled": self.context.icloud_monitor_enabled, - "icloud_monitor_running": is_icloud_monitor_running(), - "fmf_location": updated_location, - } - - async def end_icloud_monitor(): - logger.info("iCloud monitor stop requested") - self.context.icloud_monitor_enabled = False - self.context.icloud_monitor.stop() - if self.context.icloud_monitor_task is not None: - self.context.icloud_monitor_task.cancel() - with suppress(asyncio.CancelledError): - await self.context.icloud_monitor_task - self.context.icloud_monitor_task = None - if self.context.icloud_consumer_task is not None: - self.context.icloud_consumer_task.cancel() - with suppress(asyncio.CancelledError): - await self.context.icloud_consumer_task - self.context.icloud_consumer_task = None - - def is_icloud_monitor_running() -> bool: - return ( - self.context.icloud_monitor_task is not None - and not self.context.icloud_monitor_task.done() - and self.context.icloud_consumer_task is not None - and not self.context.icloud_consumer_task.done() - ) - async def pause_simulation_queue(): """Pauses asyncio.Queue playback""" self.context.simulation_queue_state = "PAUSED" @@ -769,8 +864,14 @@ class TunneldRunnerSio: self.context.simulation_queue_state = "RUNNING" update_queue_times() + def advance_simulation_queue(): + self.context.simulation_queue_state = "NEXT" + update_queue_times() + def update_queue_times(): - current_index = get_item_index(self.context.loc_id) + current_loc_id = self.context.get_current_loc_id() + current_index = get_item_index( + current_loc_id) if current_loc_id else 0 remaining_items = self.context.simulation_queue_order[current_index + 1:] new_delay = self.context.next_move or 0 now_time = datetime.now(timezone.utc) @@ -804,29 +905,47 @@ class TunneldRunnerSio: while not q.empty(): try: item = q.get_nowait() + if isinstance(item, str): + self.context.simulation_queue_pending_ids.discard(item) q.task_done() logger.info("Discarding item from queue: %s", item) except asyncio.QueueEmpty: break - clear_items() + self.context.simulation_queue_pending_ids.clear() + reset_queue() def add_item(item_id, payload): self.context.simulation_queue_data[item_id] = payload self.context.simulation_queue_order.append(item_id) def remove_item(item_id): - if item_id in self.context.simulation_queue_order: - # self.context.simulation_queue_order.remove(item_id) - self.context.simulation_queue_data[item_id]["status"] = "deleted" - - def clear_item(item_id): if item_id in self.context.simulation_queue_order: self.context.simulation_queue_order.remove(item_id) del self.context.simulation_queue_data[item_id] - def clear_items(): - self.context.simulation_queue_data = {} - self.context.simulation_queue_order = [] + def reset_queue(): + current_loc_id = self.context.get_current_loc_id() + if current_loc_id and self.context.simulation_active and current_loc_id in self.context.simulation_queue_data: + self.context.simulation_queue_order = [current_loc_id] + self.context.simulation_queue_data = { + current_loc_id: self.context.simulation_queue_data[current_loc_id] + } + else: + self.context.simulation_queue_data = {} + self.context.simulation_queue_order = [] + self.context.set_current_loc_id(None) + + + def clear_item(item_id): + if item_id in self.context.simulation_queue_order: + self.context.simulation_queue_data[item_id]["status"] = "deleted" + + def clear_future_items(): + current_loc_id = self.context.get_current_loc_id() + current_index = get_item_index( + current_loc_id) if current_loc_id else 0 + for item in self.context.simulation_queue_order[current_index + 1:]: + self.context.simulation_queue_data[item]["status"] = "deleted" def get_item(item_id): return self.context.simulation_queue_data[item_id] @@ -844,15 +963,37 @@ class TunneldRunnerSio: def get_items_in_order(): return [self.context.simulation_queue_data[i] for i in self.context.simulation_queue_order if self.context.simulation_queue_data[i].get("status") != "deleted"] - def parse_delay_seconds(raw_delay) -> int: - if raw_delay is None: - return 0 - if isinstance(raw_delay, bool): - raise ValueError("delay must be a non-negative number") - parsed = float(raw_delay) - if math.isnan(parsed) or math.isinf(parsed) or parsed < 0: - raise ValueError("delay must be a non-negative finite number") - return int(parsed) + async def enqueue_next_simulation_item() -> Optional[str]: + if self.context.simulation_queue_state == "SHUTDOWN": + return None + stale_pending_ids = { + pending_id + for pending_id in self.context.simulation_queue_pending_ids + if not isinstance(self.context.simulation_queue_data.get(pending_id), dict) + or self.context.simulation_queue_data[pending_id].get("status") != "queued" + } + if stale_pending_ids: + for stale_id in stale_pending_ids: + self.context.simulation_queue_pending_ids.discard(stale_id) + logger.info( + "Cleared stale pending queue ids before enqueue: %s", + sorted(stale_pending_ids), + ) + if self.context.simulation_queue_pending_ids: + return None + for item_id in self.context.simulation_queue_order: + if item_id in self.context.simulation_queue_pending_ids: + continue + item = self.context.simulation_queue_data.get(item_id) + if not isinstance(item, dict): + continue + if item.get("status") != "queued": + continue + self.context.simulation_queue_pending_ids.add(item_id) + await self.context.simulation_queue.put(item_id) + logger.info("Scheduled queue item %s", item_id) + return item_id + return None async def end_simulation_queue(): logger.info("End location simulation request") @@ -860,7 +1001,12 @@ class TunneldRunnerSio: end_simulation_worker(), name="end-simulation-worker" ) result = await end_task - data = {"status": result, "message": "Simulation ended"} + data = { + "command_status": "OK", + "command_class": "simulation_control", + "command": "end", + "message": "Simulation ended" + } return data async def end_simulation_worker() -> bool: @@ -875,6 +1021,8 @@ class TunneldRunnerSio: while not q.empty(): try: item = q.get_nowait() + if isinstance(item, str): + self.context.simulation_queue_pending_ids.discard(item) q.task_done() logger.info("Discarding item from queue: %s", item) except asyncio.QueueEmpty: @@ -883,7 +1031,7 @@ class TunneldRunnerSio: # Wake queue consumers blocked on queue.get(). await q.put(None) - clear_items() + reset_queue() if self.context.simulation_task is not None and not self.context.simulation_task.done(): try: @@ -901,41 +1049,50 @@ class TunneldRunnerSio: LocationSimulationQueue(dvt, self.context) as locate_simulation, ): await locate_simulation.clear() - self.context.simulation_active = False self.context.simulation_task = None self.context.set_location_enabled = True self.context.next_move = None - self.context.loc_id = None - self.context.latitude = None - self.context.longitude = None + self.context.set_current_loc_id(None) self.context.simulation_queue_state = "STOPPED" # Recreate queue to discard sentinel wakeup items and unblock clean restarts. self.context.simulation_queue = asyncio.Queue() + self.context.simulation_queue_pending_ids = set() await end_icloud_monitor() return True except Exception as e: logger.error(f"Error ending simulation queue: {e}") return False + """Switches""" + def toggle_test_mode() -> dict: self.context.test_mode = not self.context.test_mode return {"test_mode": self.context.test_mode} + def toggle_gps_noise() -> dict: + self.context.simulation_noise = not self.context.simulation_noise + return {"simulation_noise": self.context.simulation_noise} + def get_status() -> dict: + current_loc_id = self.context.get_current_loc_id() current_item = self.context.simulation_queue_data.get( - self.context.loc_id) if self.context.loc_id else None + current_loc_id) if current_loc_id else None current_start = ( current_item.get("start") if isinstance(current_item, dict) else getattr(current_item, "start", None) ) + current_latitude = current_item.get( + "latitude") if isinstance(current_item, dict) else None + current_longitude = current_item.get( + "longitude") if isinstance(current_item, dict) else None data = { "connected_clients": list(self.context.connected_clients), "current_location": { - "loc_id": self.context.loc_id, - "longitude": self.context.longitude, - "latitude": self.context.latitude, + "loc_id": current_loc_id, + "longitude": current_longitude, + "latitude": current_latitude, "start": current_start, }, "device_name": self.context.device_name, @@ -954,6 +1111,7 @@ class TunneldRunnerSio: "data": self.context.simulation_queue_data, "order": self.context.simulation_queue_order, "state": self.context.simulation_queue_state, + "gps_noise": self.context.simulation_noise, "worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None, }, "set_location_enable": self.context.set_location_enabled, @@ -963,27 +1121,24 @@ class TunneldRunnerSio: } return data - import numpy as np - - def add_gps_noise(lat, lon, std_dev_meters=5): - """ - Simulates GPS noise by adding Gaussian noise to coordinates. - 1 degree of latitude ~ 111,000 meters. - 1 degree of longitude ~ 111,000 * cos(latitude) meters. - """ - # Earth's radius, meters - earth_radius = 6378137 - - # Convert meters to degrees - lat_diff = (std_dev_meters / earth_radius) * (180 / np.pi) - lon_diff = (std_dev_meters / (earth_radius * - np.cos(np.radians(lat)))) * (180 / np.pi) - - # Generate Gaussian noise - noised_lat = lat + np.random.normal(0, lat_diff) - noised_lon = lon + np.random.normal(0, lon_diff) - - return noised_lat, noised_lon + async def get_reverse_geocode(data): + latitude = ( + data.get("latitude") + if isinstance(data, dict) + else getattr(data, "latitude", None) + ) + longitude = ( + data.get("longitude") + if isinstance(data, dict) + else getattr(data, "longitude", None) + ) + if latitude and longitude: + coords = f"{latitude}, {longitude}" + rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude) + logger.info("Reverse Geocoded %s to %s", coords, rev_geocode) + return rev_geocode + else: + return None """ FastAPI HTTP Functions""" @@ -997,7 +1152,7 @@ class TunneldRunnerSio: ) """ Device Functions """ - + """ name """ @self._app.get("/device/name") async def rsd_info(): """Get rsd information""" @@ -1017,7 +1172,7 @@ class TunneldRunnerSio: lockdown = await create_using_usbmux( serial=active_tunnel.udid, autopair=False ) - tunnels[active_tunnel.udid] = iterate_multidim( + tunnels[active_tunnel.udid] = parse_data( lockdown.all_values) except Exception as e: logger.error( @@ -1212,7 +1367,9 @@ class TunneldRunnerSio: async def app_start_icloud_monitor() -> fastapi.Response: await start_icloud_monitor() data = { - "status": "started", + "command_status": "OK", + "command_class": "icloud_monitor", + "command": "start", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } @@ -1222,7 +1379,9 @@ class TunneldRunnerSio: async def app_stop_icloud_monitor() -> fastapi.Response: await end_icloud_monitor() data = { - "status": "stopped", + "command_status": "OK", + "command_class": "icloud_monitor", + "command": "stop", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } @@ -1231,7 +1390,9 @@ class TunneldRunnerSio: @self._app.get("/icloud-monitor/status") async def app_icloud_monitor_status() -> fastapi.Response: data = { - "status": "ok", + "command_status": "OK", + "command_class": "icloud_monitor", + "command": "status", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } @@ -1262,21 +1423,21 @@ class TunneldRunnerSio: """Clear the simulation queue""" logger.info("Simulation Start Requested ") await empty_simulation_queue() - data = {"status": "cleared", "message": "Simulation cleared"} + data = {"command_status": "OK", "command_class": "simulation_control", "command": "clear", "message": "Simulation cleared"} return generate_http_response(data) @self._app.get("/simulation/pause") async def app_pause_queue() -> fastapi.Response: """Pause the simulation queue""" await pause_simulation_queue() - data = {"status": "paused", "message": "Simulation paused"} + data = {"command_status": "OK", "command_class": "simulation_control", "command": "pause", "message": "Simulation paused"} return generate_http_response(data) @self._app.get("/simulation/resume") async def app_resume_queue() -> fastapi.Response: """Resume the simulation queue""" await resume_simulation_queue() - data = {"status": "resumed", "message": "Simulation resumed"} + data = {"command_status": "OK", "command_class": "simulation_control", "command": "resume", "message": "Simulation resumed"} return generate_http_response(data) @self._app.get("/simulation/end") @@ -1288,12 +1449,27 @@ class TunneldRunnerSio: @self._app.get("/simulation/test") async def app_simulation_test_mode() -> fastapi.Response: """Enable test mode for the simulation queue""" + response = {} before_toggle = self.context.test_mode - data = toggle_test_mode() - data['status'] = "OK" - data['message'] = f"Test mode toggled from { + response['data'] = toggle_test_mode() + response['command_status'] = "OK" + response['command_class'] = "simulation_control" + response['command'] = "test_mode" + response['message'] = f"Test mode toggled from { before_toggle} to {self.context.test_mode}" - return generate_http_response(data) + return generate_http_response(response) + + @self._app.get("/simulation/noise") + async def app_simulation_noise() -> fastapi.Response: + response = {} + before_toggle = self.context.simulation_noise + response['data'] = toggle_gps_noise() + response['command_status'] = "OK" + response['command_class'] = "simulation_control" + response['command'] = "noise" + response['message'] = f"GPS noise toggled from { + before_toggle} to {self.context.simulation_noise}" + return generate_http_response(response) """Status Functions""" @@ -1315,20 +1491,7 @@ class TunneldRunnerSio: @self._app.post("/rev_geocode") async def app_proxy_osm(data: LatLng): logger.info("OSM Proxy Request, data: %s", data) - latitude = ( - data.get("latitude") - if isinstance(data, dict) - else getattr(data, "latitude", None) - ) - longitude = ( - data.get("longitude") - if isinstance(data, dict) - else getattr(data, "longitude", None) - ) - if latitude and longitude: - coords = f"{latitude}, {longitude}" - rev_geocode = await self.context.reverse_geocode.get_address(latitude, longitude) - logger.info("Reverse Geocoded %s to %s", coords, rev_geocode) + rev_geocode = await get_reverse_geocode(data) return generate_http_response(rev_geocode) @self._app.get("/ors/status") @@ -1370,7 +1533,7 @@ class TunneldRunnerSio: logger.info("headers: %s", headers) logger.info("method: %s", method) logger.info("url: %s", url) - + async with httpx.AsyncClient() as client: response = await client.request(method, url, json=body) return response.json() @@ -1440,8 +1603,9 @@ class TunneldRunnerSio: ) await device_shutdown(delay) return { + "command_status": "OK", + "command_class": "device_control", "command": "shutdown", - "status": "success", "message": f"Device shutdown initiated with {delay} seconds delay", } @@ -1452,15 +1616,17 @@ class TunneldRunnerSio: ) await device_reboot(delay) return { + "command_status": "OK", + "command_class": "device_control", "command": "reboot", - "status": "success", "message": f"Device reboot initiated with {delay} seconds delay", } case _: return { + "command_status": "ERROR", + "command_class": "device_control", "command": command, - "status": "error", "message": f"Invalid command: {command}", } @@ -1477,11 +1643,16 @@ class TunneldRunnerSio: ) try: match command: + case "next": + resp = advance_simulation_queue() + return resp + case "test-mode": data = toggle_test_mode() return { "command": command, - "status": "OK", + "command_class": "simulation_control", + "command_status": "OK", "message": "test-mode toggled", "data": data } @@ -1490,20 +1661,24 @@ class TunneldRunnerSio: """ Add a location to the simulation queue""" resp = await add_location_to_simulation_queue(data) return resp + case "clear": """ Clear the simulation queue""" - await empty_simulation_queue() + await clear_future_items() return { "command": command, - "status": "cleared", + "command_class": "simulation_control", + "command_status": "OK", "message": "Simulation cleared", } + case "pause": """ Pause the simulation queue""" await pause_simulation_queue() return { "command": command, - "status": "paused", + "command_class": "simulation_control", + "command_status": "OK", "message": "Simulation paused", } case "resume": @@ -1511,7 +1686,8 @@ class TunneldRunnerSio: await resume_simulation_queue() return { "command": command, - "status": "resumed", + "command_status": "OK", + "command_class": "simulation_control", "message": "Simulation resumed", } case "end": @@ -1526,14 +1702,52 @@ class TunneldRunnerSio: "Start location simulation request from %s", sid) data = await start_simulation_queue() return data + case "gps-noise": + """ Toggle GPS noise""" + before_toggle = self.context.simulation_noise + data = toggle_gps_noise() + data['command_status'] = "OK" + data['command_class'] = "simulation_control" + data['message'] = f"GPS noise toggled from { + before_toggle} to {self.context.simulation_noise}" + data['command'] = command + return data case _: logger.warning( "Invalid command received from %s: %s", sid, command ) - return {"status": "error", "message": "Invalid command"} + return {"command_status": "ERROR", "message": "Invalid command"} finally: await sio_send_status(sid) + @self.context.sio.event + async def location_item_update(sid, data): + """Location Item Control""" + loc_id = ( + data.get("loc_id") + if isinstance(data, dict) + else getattr(data, "loc_id", None) + ) + key = ( + data.get("key") + if isinstance(data, dict) + else getattr(data, "key", None) + ) + value = ( + data.get("value") + if isinstance(data, dict) + else getattr(data, "value", None) + ) + if loc_id and key and value: + old_val = self.context.simulation_queue_data[loc_id][key] + self.context.simulation_queue_data[loc_id][key] = value + logger.info( + "Location Item Update: %s: %s changed from %s to %s", loc_id, key, old_val, value + ) + return {"command_status": "OK", "command": "update", "command_class": "location_item", "message": "Location Item Updated", "data": self.context.simulation_queue_data[loc_id]} + else: + return {"command_status": "ERROR", "command": "update", "command_class": "location_item", "message": "Invalid request, Location Item Unchanged", "data": self.context.simulation_queue_data[loc_id]} + @self.context.sio.event async def icloud_monitor_control(sid, data): command = ( @@ -1550,7 +1764,8 @@ class TunneldRunnerSio: await start_icloud_monitor() return { "command": command, - "status": "running", + "command_class": "icloud_monitor_control", + "command_status": "OK", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } @@ -1558,25 +1773,27 @@ class TunneldRunnerSio: await end_icloud_monitor() return { "command": command, - "status": "stopped", + "command_class": "icloud_monitor_control", + "command_status": "OK", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } case "status": return { "command": command, - "status": "ok", + "command_class": "icloud_monitor_control", + "command_status": "OK", "icloud_monitor_enabled": self.context.icloud_monitor_enabled, "icloud_monitor_running": is_icloud_monitor_running(), } case "refresh": data = await refresh_icloud_location() - data["command"] = command return data case _: return { "command": command, - "status": "error", + "command_class": "icloud_monitor_control", + "command_status": "ERROR", "message": "Invalid command", } finally: @@ -1601,14 +1818,17 @@ class TunneldRunnerSio: self._tunneld_core.start() logger.info("Tunneld started successfully") return { - "status": "running", + "command_status": "OK", + "command_class": "tunneld_control", + "command": command, "message": "Tunneld started successfully", } except Exception as e: logger.error("Error starting tunneld: %s", e) return { "command": command, - "status": "error", + "command_class": "tunneld_control", + "command_status": "ERROR", "message": f"Error starting tunneld: {e}", } @@ -1618,7 +1838,9 @@ class TunneldRunnerSio: "Start tunneld watcher request from %s: %s", sid, data) await start_tunnel_watcher() return { - "status": "running", + "command_status": "OK", + "command_class": "tunneld_control", + "command": command, "message": "Tunneld watcher started successfully", } @@ -1628,7 +1850,9 @@ class TunneldRunnerSio: "End tunneld watcher request from %s: %s", sid, data) await end_tunnel_watcher() return { - "status": "stopped", + "command_status": "OK", + "command_class": "tunneld_control", + "command": command, "message": "Tunneld watcher stopped successfully", } @@ -1639,15 +1863,17 @@ class TunneldRunnerSio: try: os.kill(os.getpid(), signal.SIGINT) return { + "command_status": "OK", + "command_class": "tunneld_control", "command": command, - "status": "Success", "message": "Server shutting down...", } except Exception as e: logger.error("Error shutting down tunneld: %s", e) return { "command": command, - "status": "error", + "command_class": "tunneld_control", + "command_status": "ERROR", "message": f"Error shutting down tunneld: {e}", } @@ -1658,14 +1884,16 @@ class TunneldRunnerSio: self._tunneld_core.clear() return { "command": command, - "status": "Success", + "command_class": "tunneld_control", + "command_status": "OK", "message": "Cleared tunnels...", } except Exception as e: logger.error("Error clearing tunnels: %s", e) return { "command": command, - "status": "error", + "command_class": "tunneld_control", + "command_status": "ERROR", "message": f"Error clearing tunnels: {e}", } @@ -1684,7 +1912,8 @@ class TunneldRunnerSio: self._tunneld_core.cancel(udid=udid) return { "command": command, - "status": "Success", + "command_class": "tunneld_control", + "command_status": "OK", "udid": udid, "message": f"tunnel {udid} Canceled ...", } @@ -1692,14 +1921,16 @@ class TunneldRunnerSio: logger.error("Error canceling tunnel: %s", e) return { "command": command, - "status": "error", + "command_class": "tunneld_control", + "command_status": "ERROR", "message": f"Error canceling tunnel: {e}", } case _: return { "command": command, - "status": "error", + "command_class": "tunneld_control", + "command_status": "ERROR", "message": f"Unknown operation: {command}", } @@ -1732,11 +1963,13 @@ class LocationSimulationQueue(LocationSimulation): async def _get_dt_simulate_location(self): if DtSimulateLocation is None: - raise RuntimeError("DtSimulateLocation is not available in this pymobiledevice3 build") + raise RuntimeError( + "DtSimulateLocation is not available in this pymobiledevice3 build") if self._dt_simulate_location is None: lockdown = getattr(self.provider, "lockdown", None) if lockdown is None: - raise RuntimeError("DVT provider does not expose lockdown provider for fallback simulation") + raise RuntimeError( + "DVT provider does not expose lockdown provider for fallback simulation") self._dt_simulate_location = DtSimulateLocation(lockdown) return self._dt_simulate_location @@ -1800,7 +2033,7 @@ class LocationSimulationQueue(LocationSimulation): continue if not self.context.simulation_noise: continue - if self.context.loc_id != loc_id: + if self.context.get_current_loc_id() != loc_id: break noised_latitude, noised_longitude = self._add_gps_noise( base_latitude, base_longitude @@ -1832,7 +2065,35 @@ class LocationSimulationQueue(LocationSimulation): "worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None, } } - self.context.sio.emit("queue_data_update", {"data": data}, namespace="/") + self.context.sio.emit("queue_data_update", { + "data": data}, namespace="/") + + async def _update_location_item(self, loc_id: str) -> None: + await self.context.sio.emit( + "location_item_update", + { + "loc_id": loc_id, + "data": self.context.simulation_queue_data[loc_id] + }, + namespace="/", + ) + + async def _enqueue_next_queue_item(self) -> Optional[str]: + if self.context.simulation_queue_state == "SHUTDOWN": + return None + for item_id in self.context.simulation_queue_order: + if item_id in self.context.simulation_queue_pending_ids: + continue + item = self.context.simulation_queue_data.get(item_id) + if not isinstance(item, dict): + continue + if item.get("status") != "queued": + continue + self.context.simulation_queue_pending_ids.add(item_id) + await self.context.simulation_queue.put(item_id) + logger.info("Worker scheduled queue item %s", item_id) + return item_id + return None async def play_queue( self, disable_sleep: bool = False, timing_randomness_range: int = 0 @@ -1844,6 +2105,7 @@ class LocationSimulationQueue(LocationSimulation): continue if self.context.simulation_queue_state == "SHUTDOWN": break + await self._enqueue_next_queue_item() loc_id = await self.context.simulation_queue.get() if loc_id is None: self.context.simulation_queue.task_done() @@ -1854,25 +2116,33 @@ class LocationSimulationQueue(LocationSimulation): "Simulation queue item missing for loc_id=%s; skipping stale entry", loc_id, ) + self.context.simulation_queue_pending_ids.discard(loc_id) self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() + continue + new_status = location_item.get("status") + if new_status == "deleted": + self.context.simulation_queue_pending_ids.discard(loc_id) + self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() continue new_latitude = location_item.get("latitude") new_longitude = location_item.get("longitude") new_delay = location_item.get("delay") new_delay = 0 if new_delay is None else new_delay new_start = location_item.get("start") - + current_loc_id = self.context.get_current_loc_id() current_location_item = self.context.simulation_queue_data.get( - self.context.loc_id) + current_loc_id) if current_loc_id else None current_latitude = ( current_location_item.get("latitude") if isinstance(current_location_item, dict) - else self.context.latitude + else None ) current_longitude = ( current_location_item.get("longitude") if isinstance(current_location_item, dict) - else self.context.longitude + else None ) current_start = ( current_location_item.get("start") @@ -1881,73 +2151,79 @@ class LocationSimulationQueue(LocationSimulation): ) if self.context.set_location_enabled: - if new_delay > 0 and not disable_sleep: - countdown_delay = int(round(float(new_delay))) - if timing_randomness_range > 0: - new_delay = new_delay + random.uniform( - -timing_randomness_range, timing_randomness_range - ) - countdown_delay = int(round(float(new_delay))) - for i in range(max(0, countdown_delay), 0, -1): - if self.context.simulation_queue_state == "SHUTDOWN": - break - while self.context.simulation_queue_state == "PAUSED": - await asyncio.sleep(0.1) - if self.context.simulation_queue_state == "SHUTDOWN": - break - if self.context.simulation_queue_state == "SHUTDOWN": - break - self.context.next_move = i - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": current_latitude, - "longitude": current_longitude, - "start": current_start, - "next_move": i, - }, - namespace="/", - ) - await asyncio.sleep(1) - if self.context.simulation_queue_state == "SHUTDOWN": - self.context.simulation_queue.task_done() + while self.context.simulation_queue_data[loc_id]["delay"] > 0: + if self.context.simulation_queue_state == "NEXT": + self.context.simulation_queue_state = "RUNNING" break - self.context.simulation_queue_data[loc_id]["start"] = datetime.now( + if self.context.simulation_queue_state == "SHUTDOWN": + break + while self.context.simulation_queue_state == "PAUSED": + await asyncio.sleep(0.1) + if self.context.simulation_queue_state == "SHUTDOWN": + break + if self.context.simulation_queue_state == "NEXT": + self.context.simulation_queue_state = "RUNNING" + break + if self.context.simulation_queue_state == "SHUTDOWN": + break + self.context.next_move = location_item.get("delay") - 1 + self.context.simulation_queue_data[loc_id]["delay"] -= 1 + await self.context.sio.emit( + "simulation_status", + { + "status": self.context.simulation_active, + "loc_id": current_loc_id, + "latitude": current_latitude, + "longitude": current_longitude, + "start": current_start, + "next_move": self.context.next_move, + }, + namespace="/", + ) + await self._update_location_item(loc_id) + await asyncio.sleep(1) + if self.context.simulation_queue_state == "SHUTDOWN": + self.context.simulation_queue_pending_ids.discard(loc_id) + self.context.simulation_queue.task_done() + break + self.context.simulation_queue_data[loc_id]["start"] = datetime.now( + timezone.utc).isoformat() + if current_loc_id is not None: + self.context.simulation_queue_data[current_loc_id]["status"] = "done" + self.context.simulation_queue_data[current_loc_id]["end"] = datetime.now( timezone.utc).isoformat() - if self.context.loc_id is not None: - self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now( - timezone.utc).isoformat() - self._update_queue_data() - - await self._stop_noise_task() - await self.set(new_latitude, new_longitude) - self.context.loc_id = loc_id - self.context.latitude = new_latitude - self.context.longitude = new_longitude - if self.context.simulation_noise: - self._start_noise_task( - loc_id, new_latitude, new_longitude) - await self.context.sio.emit( - "simulation_status", - { - "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": self.context.latitude, - "longitude": self.context.longitude, - "start": new_start, - "next_move": None, - }, - namespace="/", - ) - logger.info( - "Set simulated location to %s, %s after %ss delay", - new_latitude, - new_longitude, - new_delay, - ) - self.context.simulation_queue.task_done() + await self._update_location_item(current_loc_id) + self._update_queue_data() + await self._stop_noise_task() + await self.set(new_latitude, new_longitude) + self.context.simulation_queue_data[loc_id]["status"] = "set" + self.context.set_current_loc_id(loc_id) + if self.context.simulation_noise: + self._start_noise_task( + loc_id, new_latitude, new_longitude) + active_loc_id = self.context.get_current_loc_id() + await self.context.sio.emit( + "simulation_status", + { + "status": self.context.simulation_active, + "loc_id": active_loc_id, + "latitude": new_latitude, + "longitude": new_longitude, + "start": new_start, + "next_move": None, + }, + namespace="/", + ) + logger.info( + "Set simulated location to %s, %s after %ss delay", + new_latitude, + new_longitude, + new_delay, + ) + self._update_queue_data() + self.context.simulation_queue_pending_ids.discard(loc_id) + self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() finally: await self._stop_noise_task() @@ -1956,6 +2232,8 @@ class LocationSimulationTestQueue(LocationSimulationBase): def __init__(self, context: LocationSimulationState): super().__init__() self.context = context + self._noise_task: Optional[asyncio.Task] = None + self._noise_loc_id: Optional[str] = None def __enter__(self): return self @@ -1974,10 +2252,13 @@ class LocationSimulationTestQueue(LocationSimulationBase): while not q.empty(): try: item = q.get_nowait() + if isinstance(item, str): + self.context.simulation_queue_pending_ids.discard(item) q.task_done() logger.info("Discarding item from queue: %s", item) except asyncio.QueueEmpty: break + self.context.simulation_queue_pending_ids.clear() await q.put(None) if self.context.simulation_task is not None and not self.context.simulation_task.done(): @@ -1990,102 +2271,215 @@ class LocationSimulationTestQueue(LocationSimulationBase): self.context.simulation_active = False self.context.simulation_queue_state = "SHUTDOWN" + @staticmethod + def _add_gps_noise(lat: float, lon: float, std_dev_meters: float = 5.0) -> tuple[float, float]: + """Apply Gaussian jitter in meters and convert to lat/lon deltas.""" + earth_radius = 6378137.0 + lat_sigma_deg = (std_dev_meters / earth_radius) * (180.0 / math.pi) + cos_lat = math.cos(math.radians(lat)) + if abs(cos_lat) < 1e-6: + cos_lat = 1e-6 + lon_sigma_deg = (std_dev_meters / (earth_radius * + cos_lat)) * (180.0 / math.pi) + noised_lat = lat + random.gauss(0.0, lat_sigma_deg) + noised_lon = lon + random.gauss(0.0, lon_sigma_deg) + return noised_lat, noised_lon + + async def _stop_noise_task(self) -> None: + if self._noise_task is not None: + self._noise_task.cancel() + with suppress(asyncio.CancelledError): + await self._noise_task + self._noise_task = None + self._noise_loc_id = None + + async def _noise_loop(self, loc_id: str, base_latitude: float, base_longitude: float) -> None: + while True: + await asyncio.sleep(random.randint(45, 180)) + if not self.context.simulation_active: + break + if self.context.simulation_queue_state == "SHUTDOWN": + break + if not self.context.set_location_enabled: + continue + if not self.context.simulation_noise: + continue + if self.context.get_current_loc_id() != loc_id: + break + noised_latitude, noised_longitude = self._add_gps_noise( + base_latitude, base_longitude + ) + await self.set(noised_latitude, noised_longitude) + logger.info( + "Applied simulation noise to active location loc_id=%s sent=%s,%s base=%s,%s", + loc_id, + noised_latitude, + noised_longitude, + base_latitude, + base_longitude, + ) + + def _start_noise_task(self, loc_id: str, base_latitude: float, base_longitude: float) -> None: + self._noise_loc_id = loc_id + self._noise_task = asyncio.create_task( + self._noise_loop(loc_id, base_latitude, base_longitude), + name=f"simulation-noise-{loc_id}", + ) + + def _update_queue_data(self): + data = { + "simulation_queue": { + "active": self.context.simulation_active, + "data": self.context.simulation_queue_data, + "order": self.context.simulation_queue_order, + "state": self.context.simulation_queue_state, + "worker_task": self.context.simulation_task.get_name() if self.context.simulation_task else None, + } + } + self.context.sio.emit("queue_data_update", { + "data": data}, namespace="/") + + async def _update_location_item(self, loc_id: str) -> None: + await self.context.sio.emit( + "location_item_update", + { + "loc_id": loc_id, + "data": self.context.simulation_queue_data[loc_id] + }, + namespace="/", + ) + + async def _enqueue_next_queue_item(self) -> Optional[str]: + if self.context.simulation_queue_state == "SHUTDOWN": + return None + for item_id in self.context.simulation_queue_order: + if item_id in self.context.simulation_queue_pending_ids: + continue + item = self.context.simulation_queue_data.get(item_id) + if not isinstance(item, dict): + continue + if item.get("status") != "queued": + continue + self.context.simulation_queue_pending_ids.add(item_id) + await self.context.simulation_queue.put(item_id) + logger.info("Worker scheduled queue item %s", item_id) + return item_id + return None + async def play_queue( self, disable_sleep: bool = False, timing_randomness_range: int = 0 ) -> None: - while True: - if self.context.simulation_queue_state == "PAUSED": - await asyncio.sleep(0.1) - continue - if self.context.simulation_queue_state == "SHUTDOWN": - break - loc_id = await self.context.simulation_queue.get() - if loc_id is None: - self.context.simulation_queue.task_done() - break - location_item = self.context.simulation_queue_data.get(loc_id) - if location_item is None: - logger.warning( - "Test simulation queue item missing for loc_id=%s; skipping stale entry", - loc_id, + try: + while True: + if self.context.simulation_queue_state == "PAUSED": + await asyncio.sleep(0.1) + continue + if self.context.simulation_queue_state == "SHUTDOWN": + break + await self._enqueue_next_queue_item() + loc_id = await self.context.simulation_queue.get() + if loc_id is None: + self.context.simulation_queue.task_done() + break + location_item = self.context.simulation_queue_data.get(loc_id) + if location_item is None: + logger.warning( + "Test simulation queue item missing for loc_id=%s; skipping stale entry", + loc_id, + ) + self.context.simulation_queue_pending_ids.discard(loc_id) + self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() + continue + new_status = location_item.get("status") + if new_status == "deleted": + self.context.simulation_queue_pending_ids.discard(loc_id) + self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() + continue + new_latitude = location_item.get("latitude") + new_longitude = location_item.get("longitude") + new_delay = location_item.get("delay") + new_delay = 0 if new_delay is None else new_delay + new_start = location_item.get("start") + current_loc_id = self.context.get_current_loc_id() + current_location_item = self.context.simulation_queue_data.get( + current_loc_id) if current_loc_id else None + current_latitude = ( + current_location_item.get("latitude") + if isinstance(current_location_item, dict) + else None ) - self.context.simulation_queue.task_done() - continue - new_latitude = location_item.get("latitude") - new_longitude = location_item.get("longitude") - new_delay = location_item.get("delay") - new_delay = 0 if new_delay is None else new_delay - new_start = location_item.get("start") - - current_location_item = self.context.simulation_queue_data.get( - self.context.loc_id) - current_latitude = ( - current_location_item.get("latitude") - if isinstance(current_location_item, dict) - else self.context.latitude - ) - current_longitude = ( - current_location_item.get("longitude") - if isinstance(current_location_item, dict) - else self.context.longitude - ) - current_start = ( - current_location_item.get("start") - if isinstance(current_location_item, dict) - else None - ) - - if self.context.set_location_enabled: - if new_delay > 0 and not disable_sleep: - countdown_delay = int(round(float(new_delay))) - if timing_randomness_range > 0: - new_delay = new_delay + random.uniform( - -timing_randomness_range, timing_randomness_range - ) - countdown_delay = int(round(float(new_delay))) - for i in range(max(0, countdown_delay), 0, -1): + current_longitude = ( + current_location_item.get("longitude") + if isinstance(current_location_item, dict) + else None + ) + current_start = ( + current_location_item.get("start") + if isinstance(current_location_item, dict) + else None + ) + if self.context.set_location_enabled: + while self.context.simulation_queue_data[loc_id]["delay"] > 0: + if self.context.simulation_queue_state == "NEXT": + self.context.simulation_queue_state = "RUNNING" + break if self.context.simulation_queue_state == "SHUTDOWN": break while self.context.simulation_queue_state == "PAUSED": await asyncio.sleep(0.1) if self.context.simulation_queue_state == "SHUTDOWN": break + if self.context.simulation_queue_state == "NEXT": + self.context.simulation_queue_state = "RUNNING" + break if self.context.simulation_queue_state == "SHUTDOWN": break - self.context.next_move = i + self.context.next_move = location_item.get("delay") - 1 + self.context.simulation_queue_data[loc_id]["delay"] -= 1 await self.context.sio.emit( "simulation_status", { "status": self.context.simulation_active, - "loc_id": self.context.loc_id, + "loc_id": current_loc_id, "latitude": current_latitude, "longitude": current_longitude, "start": current_start, - "next_move": i, + "next_move": self.context.next_move, }, namespace="/", ) + await self._update_location_item(loc_id) await asyncio.sleep(1) if self.context.simulation_queue_state == "SHUTDOWN": + self.context.simulation_queue_pending_ids.discard(loc_id) self.context.simulation_queue.task_done() break self.context.simulation_queue_data[loc_id]["start"] = datetime.now( timezone.utc).isoformat() - if self.context.loc_id is not None: - self.context.simulation_queue_data[self.context.loc_id]["end"] = datetime.now( + if current_loc_id is not None: + self.context.simulation_queue_data[current_loc_id]["status"] = "done" + self.context.simulation_queue_data[current_loc_id]["end"] = datetime.now( timezone.utc).isoformat() + await self._update_location_item(current_loc_id) + self._update_queue_data() + await self._stop_noise_task() await self.set(new_latitude, new_longitude) - self.context.loc_id = loc_id - self.context.loc_id = loc_id - self.context.latitude = new_latitude - self.context.longitude = new_longitude + self.context.simulation_queue_data[loc_id]["status"] = "set" + await self._update_location_item(loc_id) + self.context.set_current_loc_id(loc_id) + if self.context.simulation_noise: + self._start_noise_talk( + loc_id, new_latitude, new_longitude) + active_loc_id = self.context.get_current_loc_id() await self.context.sio.emit( "simulation_status", { "status": self.context.simulation_active, - "loc_id": self.context.loc_id, - "latitude": self.context.latitude, - "longitude": self.context.longitude, + "loc_id": active_loc_id, + "latitude": new_latitude, + "longitude": new_longitude, "start": new_start, "next_move": None, }, @@ -2097,4 +2491,9 @@ class LocationSimulationTestQueue(LocationSimulationBase): new_longitude, new_delay, ) + self._update_queue_data() + self.context.simulation_queue_pending_ids.discard(loc_id) self.context.simulation_queue.task_done() + await self._enqueue_next_queue_item() + finally: + await self._stop_noise_talk()