Giter VIP home page Giter VIP logo

findmy.py's Introduction

FindMy.py

The all-in-one library that provides everything you need to query Apple's FindMy network!

The current "Find My-scene" is quite fragmented, with code being all over the place across multiple repositories, written by several authors. This project aims to unify this scene, providing common building blocks for any application wishing to integrate with the Find My network.

Important

This project is currently in Alpha. While existing functionality will likely not change much, the API design is subject to change without prior warning.

You are encouraged to report any issues you can find on the issue tracker!

Features

  • Cross-platform: no Mac needed
  • Fetch and decrypt location reports
    • Official accessories (AirTags, iDevices, etc.)
    • Custom AirTags (OpenHaystack)
  • Apple account sign-in
    • SMS 2FA support
    • Trusted Device 2FA support
  • Scan for nearby FindMy-devices
    • Decode their info, such as public keys and status bytes
  • Import or create your own accessory keys
  • Both async and sync APIs

Roadmap

  • Local anisette generation (without server)
    • More information: #2

Installation

The package can be installed from PyPi:

pip install findmy

For usage examples, see the examples directory. Documentation can be found here.

Contributing

Want to contribute code? That's great! For new features, please open an issue first so we can discuss.

This project uses Ruff for linting and formatting. Before opening a pull request, please ensure that your code adheres to these rules. There are pre-commit hooks included to help you with this, which you can set up as follows:

pip install poetry ruff
poetry install  # this installs pre-commit into your environment
pre-commit install

After following the above steps, your code will be linted and formatted automatically before committing it.

Derivative projects

There are several other cool projects based on this library! Some of them have been listed below, make sure to check them out as well.

  • OfflineFindRecovery - Set of scripts to be able to precisely locate your lost MacBook via Apple's Offline Find through Bluetooth Low Energy.
  • SwiftFindMy - Swift port of FindMy.py

Credits

While I designed the library, the vast majority of actual functionality is made possible by the following wonderful people and organizations:

findmy.py's People

Contributors

hajekj avatar malmeloo avatar renovate[bot] avatar robertsmd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

findmy.py's Issues

Apple server does not honor time window

I'm trying to get data only for one day by setting startdate and enddate for the search.
This was the query. You see it's 24 hours.
{'search': [{'startDate': 1712449083967, 'endDate': 1712535483967, 'ids':...
But the Apple server always returns a week of data.
Is the format wrong?
Your code in accounts.py changed:

def fetch_last_reports(
        self,
        keys: Sequence[KeyPair],
        hours: int = 1 * 24,
    ) -> MaybeCoro[dict[KeyPair, list[LocationReport]]]:

Trusted device 2FA

Support trusted device 2FA. Need someone with an Apple device to test this for me, so please let me know if you're willing to help!

Add MAP to HomeAssistant

Hello @malmeloo,
I have created a script that can export all the location then send a HTML file to HomeAssistant:
image

Here is the code, If you want try :-D

"""
Example showing how to fetch locations of an AirTag, or any other FindMy accessory.
"""
from __future__ import annotations

import logging
import sys
from pathlib import Path

from _login import get_account_sync

from findmy import FindMyAccessory
from findmy.reports import RemoteAnisetteProvider

######### CSV and MAP CREATION
import csv
from gmplot import gmplot
from math import radians, cos, sin, asin, sqrt
from dateutil import parser as dtparser

######### Send to HomeAssistant
import paramiko
from paramiko import SSHClient
from scp import SCPClient

########################################### Configuration ###########################################
# URL to (public or local) anisette server
ANISETTE_SERVER = "http://localhost:6969"


# Name of the Airtag, used also for naming CSV file and HTML map File
person = "PersonName"

# Map Configuration
# Home LAT AND LON, See Google Maps or HA to find this Info
LAT = 41.80
LON = 12.62

#ZOOM level, where 0 is fully zoomed out. Defaults to 13.
ZOOM = 12

# GMAPS ApiKey https://cloud.google.com/maps-platform/ click Get Started. Choose Maps and follow the instructions. Set restrictions for the API Key (Click on Maps Javascript API > Credentials > Your api Key > API restrictions and select Maps Javascript API).
apikey = ""

# Create PLOT, Values are True or False
CreatePlot = True
# Color of the PolyLines, Can be hex (β€˜#00FFFF’), named (β€˜cyan’), or matplotlib-like (β€˜c’), see here https://github.com/gmplot/gmplot/blob/master/gmplot/color.py and ColorCode.pdf: 
PColor = 'cornflowerblue'
#Width of the polyline, in pixels. Defaults to 1.
PWidth = 1

# Marker COLOR Can be hex (β€˜#00FFFF’), named (β€˜cyan’), or matplotlib-like (β€˜c’) see here https://github.com/gmplot/gmplot/blob/master/gmplot/color.py and ColorCode.pdf: 
# All Markers
#AMColor = 'red'
AMColor = '#FFFFFF'
# Keep Disabled until Fix Found, If Enabled the Last Location not evident
AMColorDisabled = False
# Last Location Marker Color and Text Color see here https://github.com/gmplot/gmplot/blob/master/gmplot/color.py and ColorCode.pdf: 
#LMColor = 'chartreuse'
LMColor = '#00FF00'
MARKERLABEL = False
# Number of last marker, for print all marker set to False
NMARKER = False
# Dark Mode for the MAP, Values are True or False
DARKMODE = True

# Force Map Creation Only and Send To HA if Send to HA is Enables, Values are True or False, Should be FALSE
FORCEMAP = False

# Radius 0.05 means 50 MT from older location, so locations under 100MT from before location will be ignored
radius = 0.05

#HomeAssistant Connection Details to send MAP HTML FILE
#Send MAP to HA Instance, Values are True or False
SENDTOHA = True
HAHostName=''
HAPort = ''
HAUsername=''
HAPassword=''
########################################### END Configuration ###########################################

logging.basicConfig(level=logging.INFO)

def openfiles():
    global lasthistory, histidx
    with open('data/lasthistory.txt','r', encoding="utf8") as f:
      lasthistory = f.readline()
      if len(lasthistory) == 0:
        histidx = 0
      
def haversine(lat1, lon1, lat2, lon2):
    """
    Calculate the great circle distance between two points 
    on the earth (specified in decimal degrees)
    https://stackoverflow.com/questions/4913349/haversine-formula-in-python-bearing-and-distance-between-two-gps-points
    """
    # convert decimal degrees to radians 
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])

    # haversine formula 
    dlon = lon2 - lon1 
    dlat = lat2 - lat1 
    a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
    c = 2 * asin(sqrt(a)) 
    r = 6371 # Radius of earth in kilometers. Use 3956 for miles
    return c * r


def main(plist_path: str) -> int:
    global lasthistory, histidx
    # Step 0: create an accessory key generator
    with Path(plist_path).open("rb") as f:
        airtag = FindMyAccessory.from_plist(f)

    # Step 1: log into an Apple account
    print("Logging into account")
    anisette = RemoteAnisetteProvider(ANISETTE_SERVER)
    acc = get_account_sync(anisette)

    # step 2: fetch reports!
    print("Fetching reports")
    reports = acc.fetch_last_reports(airtag)
    
    # step 3: Order reports, last at the end
    sorted_reports = sorted(reports, key=lambda x: x.timestamp)

    # step 4: Find the index of the last printed report
    for index, report in enumerate(sorted_reports):
        if (repr(str(report.timestamp)) == repr(str(lasthistory.strip()))):
            histidx = index + 1
    
    # step 5: print 'em and save to CSV
    print()
    print("Location reports:")
    print("Total Location Report:", len(sorted_reports), "Missing Location Report to Extract:", len(sorted_reports) - histidx)
     
    if (len(sorted_reports) - histidx > 0):
        for sorted_report in sorted_reports[histidx:]:
            print(f" - {sorted_report}")
            lastreport = sorted_report
            #print(dir(sorted_report))

            latitude = sorted_report.latitude
            longitude = sorted_report.longitude
            date = sorted_report.timestamp

            values = Path("data/"+person+"_gps.csv")
            if values.is_file():
                with open('data/'+person+'_gps.csv', 'a', newline='') as newFile:
                    newFileWriter = csv.writer(newFile)
                    newFileWriter.writerow([latitude,longitude,date])
            
            else:
                with open('data/'+person+'_gps.csv','w', newline='') as newFile:
                    newFileWriter = csv.writer(newFile)
                    newFileWriter.writerow(['latitude','longitude','date'])
                    newFileWriter.writerow([latitude,longitude,date])
            
    print("Location Report Extracted: ", len(sorted_reports) - histidx)
    print("No more Location Report Fetched")

    # step 6: Save the new printed report timestamp in the DB
    try:
        lastreport.timestamp
    except: 
        pass
    else: 
        with open('data/lasthistory.txt', 'w') as f:
            f.write(str(lastreport.timestamp))
    
    # step 7: If there are new location report, create a NEW MAP
        createmap()
    return 0



def createmap():
    print ("Creating HTML MAP")
    
#    map_styles = [
#        {
#            'featureType': 'all',
#            'stylers': [
#                {'saturation': -80},
#                {'lightness': 30},
#            ]
#        }
#    ]

# DARK MODE Style Settings For MAP
    map_styles = [
        { 'elementType': 'geometry', 'stylers': [{ 'color': "#242f3e" }] },
        { 'elementType': "labels.text.stroke", 'stylers': [{ 'color': "#242f3e" }] },
        { 'elementType': "labels.text.fill", 'stylers': [{ 'color': "#746855" }] },
        {
            'featureType': "administrative.locality",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#d59563" }],
        },
        {
            'featureType': "poi",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#d59563" }],
        },
        {
            'featureType': "poi.park",
            'elementType': "geometry",
            'stylers': [{ 'color': "#263c3f" }],
        },
        {
            'featureType': "poi.park",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#6b9a76" }],
        },
        {
            'featureType': "road",
            'elementType': "geometry",
            'stylers': [{ 'color': "#38414e" }],
        },
        {
            'featureType': "road",
            'elementType': "geometry.stroke",
            'stylers': [{ 'color': "#212a37" }],
        },
        {
            'featureType': "road",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#9ca5b3" }],
        },
        {
            'featureType': "road.highway",
            'elementType': "geometry",
            'stylers': [{ 'color': "#746855" }],
        },
        {
            'featureType': "road.highway",
            'elementType': "geometry.stroke",
            'stylers': [{ 'color': "#1f2835" }],
        },
        {
            'featureType': "road.highway",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#f3d19c" }],
        },
        {
            'featureType': "transit",
            'elementType': "geometry",
            'stylers': [{ 'color': "#2f3948" }],
        },
        {
            'featureType': "transit.station",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#d59563" }],
        },
        {
            'featureType': "water",
            'elementType': "geometry",
            'stylers': [{ 'color': "#17263c" }],
        },
        {
            'featureType': "water",
            'elementType': "labels.text.fill",
            'stylers': [{ 'color': "#515c6d" }],
        },
        {
            'featureType': "water",
            'elementType': "labels.text.stroke",
            'stylers': [{ 'color': "#17263c" }],
        },
    ]

# DARK MODE Style Settings For MAP








    if DARKMODE == True:
        gmap = gmplot.GoogleMapPlotter(LAT, LON, ZOOM, apikey=apikey, map_styles=map_styles, scale_control=True)
    else:
        gmap = gmplot.GoogleMapPlotter(LAT, LON, ZOOM, apikey=apikey, scale_control=True)
    
    gps = []
    locmarker = []
    
    filecsv = open('data/'+person+'_gps.csv')
    csvnumline = len(filecsv.readlines())
    
    with open('data/'+person+'_gps.csv') as csv_file:
        csv_reader = csv.reader(csv_file, delimiter=',')

        previous_row = []
        for line, row in enumerate(csv_reader):
            if line == 0:
                pass
            else:
                if previous_row:
                    prev_lat = float(previous_row[0])
                    prev_lon = float(previous_row[1])
                    lat = float(row[0])
                    lon = float(row[1])
                    a = haversine(float(prev_lat), float(prev_lon), float(lat), float(lon))
                    #print('Distance (km) : ', a)
                    if a <= radius:
                        continue
                    else:
                        gps.append((lat,lon))
                        locmarker.append((float(row[0]),float(row[1]), dtparser.parse(row[2]).strftime("%d/%m/%Y %H:%M:%S")))
                        previous_row = row

                else:
                    gps.append((float(row[0]),float(row[1])))
                    locmarker.append((float(row[0]),float(row[1]), dtparser.parse(row[2]).strftime("%d/%m/%Y %H:%M:%S")))
                    previous_row = row
    
    # Check for Marker
    for idx, row in enumerate(locmarker):
        if idx == (len(locmarker) - 1):
            if (MARKERLABEL == True):
                gmap.marker(row[0], row[1], title=row[2], info_window="<h1><p style='color:" + LMColor + "'>" + row[2] + "</p></h1>", color=LMColor, label=idx)
            else:
                gmap.marker(row[0], row[1], title=row[2], info_window="<h1><p style='color:" + LMColor + "'>" + row[2] + "</p></h1>", color=LMColor)
            
        else:
            if (NMARKER == False):
                if AMColorDisabled == True:
                    gmap.marker(row[0], row[1], title=str(row[2]), info_window="<h1>" + row[2] + "</h1>", label=idx)
                else:
                    gmap.marker(row[0], row[1], title=str(row[2]), info_window="<h1>" + row[2] + "</h1>", label=idx, color=AMColor)
            else:
                if (idx >= (len(locmarker) - NMARKER) and idx < (len(locmarker) - 1)):
                    if AMColorDisabled == True:
                        gmap.marker(row[0], row[1], title=str(row[2]), info_window="<h1>" + row[2] + "</h1>", label=idx)
                    else:
                        gmap.marker(row[0], row[1], title=str(row[2]), info_window="<h1>" + row[2] + "</h1>", label=idx, color=AMColor)

    # Polygon
    lats, lons = zip(*gps)
        
    if CreatePlot == True:    
        gmap.plot(lats, lons, color=PColor, edge_width=PWidth)
    
    gmap.draw("data/"+person+"_gps.html")
    
    if SENDTOHA == True:
        sendtohomeassistant()

def sendtohomeassistant():
    print("Send MAP To HomeAssistant Instance, Under PATH /config/www/findmy/"+person+"_gps.html" )
    ssh = SSHClient()
    #ssh.load_system_host_keys()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(hostname = HAHostName, port = HAPort, username = HAUsername, password = HAPassword)
    
    # SCPCLient takes a paramiko transport as its only argument
    scp = SCPClient(ssh.get_transport())
    
    scp.put("data/"+person+"_gps.html", "/config/www/findmy/"+person+"_gps.html")
#    scp.get('file_path_on_remote_machine', 'file_path_on_local_machine')
    scp.close()


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <path to accessory plist>", file=sys.stderr)
        print(file=sys.stderr)
        print("The plist file should be dumped from MacOS's FindMy app.", file=sys.stderr)
        sys.exit(1)
    if FORCEMAP == True:
        createmap()
    else:
        openfiles()
        sys.exit(main(sys.argv[1]))

Some location reports are not decodable

Context: #4 (comment)

There has been at least one case where a location report from a real AirTag was not decodable. Maybe this an issue on Apple's side?

In any case, this edge case should be handled better than straight up crashing.

Example:

2bd8e645000504dee6a88ab4580e4c90dcacc62e0efae57a003a1bac1212ba670398be61cb648e08417cf3714ee0429f961793aa645600a7f69807e215292da3a6bda511953cb15b4840de16e26c17651eef760e77fa4b2ca5

Use with device already in the network?

Hi,
How technically feasible is it to use this project to work with official AirTags or other Find My devices? Already working AirTag clones are being sold for $2-4 a piece on Aliexpress, so I don't see a point in spending a lot of time messing with flashing, firmwares and all of that OpenHaystack stuff, when I can just buy a working "AirTag" for so cheap.

can't retrieve location reports from 'third party Airtag'

I tested real_airtag.py with some 'Airtag compatible' AIYATO tag(link is here), and can't retrieve any location reports. The http response code is 200, but no content.

/Users/snomile/git/hass_scripts/venv3/bin/python /Users/snomile/git/hass_scripts/ha_script/airtag_location.py
Logging into account
Fetching reports
DEBUG:asyncio:Using selector: KqueueSelector
DEBUG:root:Fetching reports for 195 keys
INFO:root:Fetching anisette data from http://192.168.12.3:6969
DEBUG:root:Creating aiohttp session
DEBUG:root:Creating aiohttp session

Location reports:

Process finished with exit code 0

The tag is compatible with Findmy network, I can locate the tag via FindMy app, all functions go well and smooth, no difference compared to Airtag.

I also tested other devices such as AirPods Pro and MacBook Pro, they all work fine with real_airtag.py.

This is part of plist of AIYATO tag, please tell me what information should I provide more, thanks!
image

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Awaiting Schedule

These updates are awaiting their schedule. Click on a checkbox to get an update now.

  • chore(deps): update dependency ruff to v0.5.4
  • chore(deps): update dependency sphinx-autoapi to v3.2.1

Detected dependencies

github-actions
.github/workflows/docs.yml
  • actions/checkout v4
  • actions/setup-python v5
  • actions/configure-pages v5
  • actions/upload-pages-artifact v3
  • actions/deploy-pages v4
.github/workflows/pre-commit.yml
  • actions/checkout v4
  • actions/setup-python v5
  • pre-commit/action v3.0.1
  • pre-commit-ci/lite-action v1.0.2
.github/workflows/publish.yml
  • actions/checkout v4
  • actions/setup-python v5
  • softprops/action-gh-release v2
pep621
pyproject.toml
poetry
pyproject.toml
  • python >=3.9,<3.13
  • srp ^1.0.21
  • cryptography ^43.0.0
  • beautifulsoup4 ^4.12.3
  • aiohttp ^3.9.5
  • bleak ^0.22.2
  • pre-commit ^3.6.0
  • sphinx ^7.2.6
  • sphinx-autoapi ^3.0.0
  • pyright ^1.1.350
  • ruff 0.5.2

  • Check this box to trigger a request for Renovate to run again on this repository

shared AirTags incompatible (only key provided is `peerTrustSharedSecret`)

The decrypted plist for an AirTag that has been shared with me is the following. This is incompatible with the current library due to not having a private key. Yes, I've tried using the peerTrustSharedSecret as the private key, it doesn't work as-is.

Seems like a good way to implement this would be to examine the traffic to Apple servers when examining the location of a shared AirTag via the FindMy application. It may use a different endpoint for an intermediary step between peerTrustSharedSecret and privateKey.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>cloudKitMetadata</key>
	<data>
	#Base64 blob here#
	</data>
	<key>communicationsIdentifier</key>
	<dict>
		<key>ids</key>
		<dict>
			<key>correlationIdentifier</key>
			<string>#uuid (unknown what it is) here#</string>
			<key>destination</key>
			<dict>
				<key>destination</key>
				<string>mailto:#Owner email here#</string>
				<key>type</key>
				<integer>0</integer>
			</dict>
		</dict>
	</dict>
	<key>displayIdentifier</key>
	<string>#Owner email here#</string>
	<key>identifier</key>
	<string>#baUUID here#</string>
	<key>peerTrustSharedSecret</key>
	<dict>
		<key>key</key>
		<dict>
			<key>data</key>
			<data>
			#INSERT KEY HERE#
			</data>
		</dict>
	</dict>
	<key>type</key>
	<integer>1</integer>
</dict>
</plist>

Fix SMS 2FA ID resolver

Apple appears to have locked down the https://gsa.apple.com/auth endpoint, as it now returns a generic 403 error every time across multiple accounts, IPs and anisette generators. This isn't a huge deal, but it was used to resolve the available phone numbers for 2FA and their corresponding IDs, which are necessary for submission.

I have just tried an (unreleased) fix which simply takes the phone numbers from the SPD data in the initial auth response. This works but there are no IDs in that data, so it's currently just incrementing starting from 1. That should work for most accounts with only a single phone number.

Maybe it's worth looking into an alternative source of this data altogether; calling that endpoint had the annoying side effect of implicitly requesting a 2FA code every time you called it, which is not really compatible with the library's API design.

Need to re-login every time

Hello, I'm building a simple flask api that converts tag location to openhaystack reports (so that I can tweak the app to use real airtags)... and so I have put toghether a docker-compose with findmy inside flask and anisette.
It works the first time I login... After that I get this message:

root@63ae2a4c766f:/app# python get_reports.py 
Traceback (most recent call last):
  File "/app/get_reports.py", line 4, in <module>
    reports =  get_reports()
               ^^^^^^^^^^^^^
  File "/app/app.py", line 50, in get_reports
    reports[name] = (get_report(name))
                     ^^^^^^^^^^^^^^^^
  File "/app/app.py", line 39, in get_report
    acc.fetch_last_reports(airtag),          
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/account.py", line 1077, in fetch_last_reports
    return self._evt_loop.run_until_complete(coro)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/asyncio/base_events.py", line 654, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/account.py", line 680, in fetch_last_reports
    return await self.fetch_reports(keys, start, end)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/account.py", line 639, in fetch_reports
    return await self._reports.fetch_reports(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/reports.py", line 239, in fetch_reports
    reports.extend(await self._fetch_reports(date_from, date_to, chunk))
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/reports.py", line 260, in _fetch_reports
    data = await self._account.fetch_raw_reports(start_date, end_date, ids)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/reports/account.py", line 594, in fetch_raw_reports
    resp = r.json()
           ^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/findmy/util/http.py", line 48, in json
    return json.loads(self.text())
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
ERROR:asyncio:Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7b0291a2fc50>
ERROR:asyncio:Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7b0291be3310>, 10243.288814196)]']
connector: <aiohttp.connector.TCPConnector object at 0x7b0291a2d5d0>
ERROR:asyncio:Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7b0291a72f50>
ERROR:asyncio:Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7b0291be2d60>, 10243.775111868)]']
connector: <aiohttp.connector.TCPConnector object at 0x7b0291a1bc90>

Swift port

In case anyone is interested, I've started a Swift port of this library
Still a lot to do since only the KeyPair and Accessory classes are ported but that's enough to generate to the primary and secondary keys for any date from the original private key and shared secret

SwiftFindMy

How to obtain .plist files

I failed in the first step and found it difficult to find the path where findmy stores the. plist file. How did you find theand retrieve it? Thank you
1714468586850

Not getting back the latest reports after a while

Somehow after a while fetch_latest_reports are not getting back the latest reports any more and get stuck at some time point.

I've tried:

  • Passing in different parameters to fetch_latest_reports(hours=...) none worked
  • Re-decoding the plist file with the MacOS FindMy running and using the latest plist file, no luck

I have been polling with a 15 minutes interval --- did apple detect and put some kind of hold on my account?

Knowing if a particular AirTag presents in the scan results?

Hi!

Since #41, I am pretty convinced that close-to-realtime location using the crowdsourcing FindMy approach won't fit my application (tracking my dog). I was thinking of using an AirTag as a BLE tracker just so that I can know in almost real-time that the AirTag is at home.

Is there a way to know if a particular AirTag is present in the scan results? I see that the scanner gives the MAC address and the public key of any AirTag. But, given an AirTag, how do I know either of these two values for that particular AirTag? The public key is rotating, am I right? So maybe there is a way to predict this public key using the seed plist?

I have tried:

>>> acc = findmy.accessory.FindMyAccessory.from_plist(open("my_airtag.plist", "rb"))
>>> acc.keys_at(datetime.now(timezone.utc))

But the key it returns doesn't seem to match with any of the scanner results. I also tried passing in my current timezone (EST) to keys_at, but still no luck.

Any guidance or suggestions would be greatly appreciated!

Reduce fetch_reports() session creation

Currently, reports.py:fetch_reports() creates a new HTTPSession for every lookup, because it doesn't know when to close it. This function should probably be converted into a class to make it stateful and add a close() method to it, just like BaseAnisetteProvider and the gang.

throws meaningful exception for auth failure

I'm getting the following error for calling AppleAccount.fetch_reports():

Traceback (most recent call last):
  File "/opt/./update_findmy_locations.py", line 137, in <module>
    main()
  File "/opt/./update_findmy_locations.py", line 129, in main
    reports = fetch_reports_from_apple(acc, device, fetch_from, fetch_to)
  File "/opt/./update_findmy_locations.py", line 88, in fetch_reports_from_apple
    reports = acc.fetch_reports(list(lookup_keys), fetch_from, fetch_to)
  File "/usr/local/lib/python3.10/site-packages/findmy/reports/account.py", line 1045, in fetch_reports
    return self._evt_loop.run_until_complete(coro)
  File "/usr/local/lib/python3.10/asyncio/base_events.py", line 649, in run_until_complete
    return future.result()
  File "/usr/local/lib/python3.10/site-packages/findmy/reports/account.py", line 641, in fetch_reports
    return await self._reports.fetch_reports(
  File "/usr/local/lib/python3.10/site-packages/findmy/reports/reports.py", line 239, in fetch_reports
    reports.extend(await self._fetch_reports(date_from, date_to, chunk))
  File "/usr/local/lib/python3.10/site-packages/findmy/reports/reports.py", line 260, in _fetch_reports
    data = await self._account.fetch_raw_reports(start_date, end_date, ids)
  File "/usr/local/lib/python3.10/site-packages/findmy/reports/account.py", line 596, in fetch_raw_reports
    resp = r.json()
  File "/usr/local/lib/python3.10/site-packages/findmy/util/http.py", line 48, in json
    return json.loads(self.text())
  File "/usr/local/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/usr/local/lib/python3.10/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/local/lib/python3.10/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
ERROR:asyncio:Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7ffffb9c7790>
ERROR:asyncio:Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7ffffb93e440>, 1058485.242966568)]']
connector: <aiohttp.connector.TCPConnector object at 0x7ffffb9c7760>
ERROR:asyncio:Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7ffffb9c7910>
ERROR:asyncio:Unclosed connector
connections: ['[(<aiohttp.client_proto.ResponseHandler object at 0x7ffffb9c9f00>, 1058486.638551861)]']
connector: <aiohttp.connector.TCPConnector object at 0x7ffffb9c78b0>

The real issue is a 401 error with an empty response from iCloud API.
May we have a meaningful exception so my code knows how to handle the re-login?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.