FindMy.py icon indicating copy to clipboard operation
FindMy.py copied to clipboard

Fetching and saving state for multiple devices?

Open victorhooi opened this issue 2 months ago • 4 comments

I'm reading through the documentation here:

https://docs.mikealmel.ooo/FindMy.py/getstarted/02-fetching.html

and it mentions you can pass through a list containing multiple devices to account.fetch_location(), which will then optimise the request payload. This is pretty useful.

Just to clarify though - normally if you were passing a single device to account.fetch_location(), you could then call .to_json("path") afterwards, to persist it back to disk. However, in this case, if you use a list of devices - is the idea that you call .to_json("path") on each element in the list? And that will still work - as in, it will still figure out the right information back from the earlier account.fetch_location() call?

victorhooi avatar Oct 19 '25 15:10 victorhooi

Actually - I've just realised there's a small issue when querying for multiple devices.

The docs suggest (and airtag.py does this) calling:

device.to_json("airtag.json")

after fetching the location, to save stuff to disk - I assume this is the alignment values for each AirTag device?

However, if your input is just a list like say this:

airtags = []
bobs_flute = FindMyAccessory.from_json(Path("bobs_flute.json"))
tonys_guitar = FindMyAccessory.from_json(Path("tonys_guitar.json"))
joes_drums = FindMyAccessory.from_json(Path("joes_drums.json"))
leos_violin = FindMyAccessory.from_json(Path("leos_violin.json"))
airtags.append(bobs_flute)
airtags.append(tonys_guitar)
airtags.append(joes_drums)
airtags.append(leos_violin)

Then you call fetch_location() on the list:

location = acc.fetch_location(airtags)

If you want to persist it back to disk after - you could do this:

for airtag in airtags:
        airtag.to_json("{}.json".format(airtag.identifier))

But in my case, all the .json files had friendly names - and those names aren't persisted anywhere. (And the "name" field int he JSON field is "null").

How should you be handling multiple devices like that?

victorhooi avatar Oct 19 '25 15:10 victorhooi

There are two different ways to go about this. The first is to set the name property on the accessory object - this will then be persisted in the JSON representation.

The second, 'proper' way is still under development and a bit experimental at this time, but for the sake of documentation I'll explain it anyway.

You can use a UniformSessionManager to keep track of serializable objects of the same type, such as a bunch of accessories. I'm on my phone right now so hopefully I can recall the right way to use it, but basically:

mgr = UniformSessionManager(FindMyAccessory)
airtag1 = mgr.add_from_json("airtag1.json")
airtag2 = mgr.add_from_json("airtag2.json")

# fetch locations for devices
# reports = blabla

mgr.save()

I'm not sure if the manager is iterable, so you may or may not need to keep track of a list of accessories yourself. Either way I do want it to be iterable, and I'm thinking about making it so that you can directly pass such a manager to fetch_location in the future. But that's not implemented yet.

Edit: so to answer your first question: yes, the information from the fetch will still make its way back to the individual accessory objects. You will need to save the changes made for each individual accessory, either manually (as in your example) or using such a manager.

malmeloo avatar Oct 19 '25 18:10 malmeloo

Got it - yes, in my case, the AirTag .json files have a name field, but that's currently set to null. Hence, I'd simply renamed the files, and was using a human-readable filename to "name" each AirTag device, as I didn't want to tamper manually with the file contents.

These json files were obtained via examples/plist_to_json.py, using the output from OpenTagViewer. In that case, I know the output zipfile had a BeaconNamingRecord directory, and also a OwnedBeacons directory. For some reason, the name records were split off. It might be an idea to automatically amalgamate the two there somehow (not sure if examples/plist_to_json.py is the right place to do that, or somewhere else - I assume that script is designed to handle .plist files from a few different sources, not just OpenTagViewer).

So yeah, I guess as a stopgap, I could manually just set the name field in the JSON for now 😁.

The second method using UniformSessionManager is the "proper" method, right?

I can look into doing that, if you think it's the better way.

Can I call fetch_location() on the manager directly, or do I still need to find a way to call those on the individual AirTag devices individually?

Also - I noticed when I called fetch_location() on a list of AirTags (in this case, there were four in the list):

location = acc.fetch_location(airtags)

the output seemed to suggest it was fetching multiple reports:

I thought that behaviour was only when you called fetch_location_history():

Logged in as: [email protected] (Foo Bar)
INFO:findmy.reports.reports:Fetched 20 new reports (index 108661)
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
INFO:findmy.accessory:Updating alignment based on report observed at index 108951
WARNING:findmy.reports.account:Empty response received when fetching reports, retrying (1/3)
INFO:findmy.reports.reports:Fetched 20 new reports (index 109533)
INFO:findmy.accessory:Updating alignment based on report observed at index 109823
INFO:findmy.accessory:Updating alignment based on report observed at index 109823
INFO:findmy.reports.reports:Fetched 20 new reports (index 108701)
INFO:findmy.accessory:Updating alignment based on report observed at index 108991
INFO:findmy.accessory:Updating alignment based on report observed at index 108991
INFO:findmy.accessory:Updating alignment based on report observed at index 108991
INFO:findmy.accessory:Updating alignment based on report observed at index 108991
WARNING:findmy.reports.account:Empty response received when fetching reports, retrying (1/3)
INFO:findmy.reports.reports:Fetched 20 new reports (index 110874)
INFO:findmy.accessory:Updating alignment based on report observed at index 111164
INFO:findmy.accessory:Updating alignment based on report observed at index 111164
INFO:findmy.accessory:Updating alignment based on report observed at index 111164
INFO:findmy.accessory:Updating alignment based on report observed at index 111164
Last known location:

Or am I mis-reading the output there?

victorhooi avatar Oct 19 '25 23:10 victorhooi

Yes indeed, the plist_to_json example script is really just a helper tool to migrate to the new file format. The name of the accessory is not present in the old plist, and I wanted the script to be as simple as possible. So it doesn't currently pull the accessory name from anywhere.

Eventually I want UniformSessionManager to be the proper method; currently it's not very well-integrated with the rest of the library and it hasn't been tested much in the first place. It's just a simple collection manager for serializable objects of the same type. Since it has no connection to your account instance you do still need to call fetch_location on your account, so account.fetch_location([airtag1, airtag2, ...]). Eventually I want it to accept a manager instance directly, so something like account.fetch_location(mgr). But that hasn't been implemented yet.

Due to the way FindMy works, the library will in some cases still query multiple reports - it makes an educated guess for what the tag's current key index will be, but it doesn't know if it's 100% correct until it has received the location reports. So even if you're just asking for the latest location, it will probably still encounter some other reports in the process. These reports are not exposed when using fetch_location (to preserve API compatibility for when I change the underlying algorithm or if Apple makes server-side changes), but they are returned when using fetch_location_history, with a bit of a disclaimer on top (as you've already noticed.)

malmeloo avatar Oct 20 '25 18:10 malmeloo