fjxmlzn / FindMyHistory

Track your Apple devices and look up their past location, battery levels, and more
MIT License
308 stars 34 forks source link

Nextcloud PhoneTrack App Intergration & visualization #11

Open MitchellHicks opened 1 year ago

MitchellHicks commented 1 year ago

I think this would provide an easy visual interface for people to follow the AirTags.

This is the Nextcloud App https://apps.nextcloud.com/apps/phonetrack

This is the Page for Clients to send updates to the APP. https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/userdoc#logging-methods

It has a built in Post/Get option for adding History Points I am sure there are other options.

"HTTP request You can build your own logging system and make GET or POST HTTP requests to PhoneTrack. Here is an example of logging URL with POST: https://your.server.org/NC_PATH_IF_NECESSARY/index.php/apps/phonetrack/logPost/TOKEN/DEVNAME and with GET: https://your.server.org/NC_PATH_IF_NECESSARY/index.php/apps/phonetrack/logGet/TOKEN/DEVNAME The POST or GET parameters are:

lat (decimal latitude) lon (decimal longitude) alt (altitude in meters) timestamp (epoch timestamp in seconds) acc (accuracy in meters) bat (battery level in percent) sat (number of satellites) useragent (device user agent) speed (speed in meter per second) bearing (bearing in decimal degrees) "

Great Work on the Apple side..

fjxmlzn commented 1 year ago

That's a great proposal! I unfortunately do not have cycles in the near future to look into this. But anyone is welcome to contribute and make pull requests.

aChrisYouKnow commented 1 year ago

Chiming in to say this integration would be awesome. Unfortunately don't have the skill set to make it happen. Will be following this one for future reference though.

Yannik commented 1 year ago

Here is a very quick-and-dirty phonetrack integration:

diff --git a/lib/log_manager.py b/lib/log_manager.py
index fd8e79a..078ad78 100644
--- a/lib/log_manager.py
+++ b/lib/log_manager.py
@@ -79,12 +79,68 @@ class LogManager(object):
             writer = csv.writer(f)
             writer.writerow([data[k] for k in self._keys])

+    def _send_to_phonetrack(self, name, data):
+        #if not self.initialized:
+        import logging
+        if not hasattr(self, 'initialized'):
+            
+            logger = logging.getLogger()
+            logger.setLevel(logging.DEBUG)
+            formatter = logging.Formatter("[%(asctime)s] %(levelname)s: %(message)s")
+            
+            file_handler = logging.FileHandler('phonetrack.log')
+            file_handler.setLevel(logging.DEBUG)
+            file_handler.setFormatter(formatter)
+
+            logger.addHandler(file_handler)
+            self.initialized = True
+
+        if data['location|timeStamp'] == 'NULL':
+            logging.info("Ignoring call for %s with empty location data" % data['name'])
+            return
+
+        logging.info("New data for %s" % data['name'])
+
+        phonetrack_key = {
+            'Item Name': 'phonetrack-session-key',
+    
+        }
+        #logging.debug(data)
+
+        if data['name'] not in phonetrack_key:
+            logging.info("No phone key found for %s", data['name'])
+            return
+
+        url = 'https://XXXX/index.php/apps/phonetrack/logGet/%s/%s' % (phonetrack_key[data['name']], data["name"])
+        #logging.debug("Url: %s" % url)
+
+        params = {
+            'timestamp': data['location|timeStamp'],
+            'lat': data['location|latitude'],
+            'lon': data['location|longitude'],
+            'alt': data['location|altitude']
+        }
+
+        import requests
+        requests.get(url, params=params)
+            
+
     def refresh_log(self):
         items_dict = self._get_items_dict()
         for name in items_dict:
             if (name not in self._latest_log or
                     self._latest_log[name] != items_dict[name]):
                 self._save_log(name, items_dict[name])
+                self._send_to_phonetrack(name, items_dict[name])
                 self._latest_log[name] = items_dict[name]
                 self._log_cnt[name] += 1

Note: I will NOT provide any support for this. Use at your own risk.

aChrisYouKnow commented 11 months ago

I wanted to circle back after trying this out, but now so much time has passed, and it's still sitting on my to-do list. Figured I'd belatedly circle back and say thanks for the response. It's more than enough to play around with / build upon.

MitchellHicks commented 11 months ago

I modded the code to work.. And have been out of town.. I will try to send you what I made to get it working its been running since a week after I asked. I frankly completely forgot about it.

On December 7, 2023 1:41:05 PM PST, Chris @.***> wrote:

I wanted to circle back after trying this out, but now so much time has passed, and it's still sitting on my to-do list. Figured I'd belatedly circle back and say thanks for the response. It's more than enough to play around with / build upon.

-- Reply to this email directly or view it on GitHub: https://github.com/fjxmlzn/FindMyHistory/issues/11#issuecomment-1846153647 You are receiving this because you authored the thread.

Message ID: @.***>

aChrisYouKnow commented 11 months ago

Awesome, no rush, but would definitely appreciate it.

DShakar commented 10 months ago

Hey Mitchel, I would love to have the modded code, I just thought I would reply to remind you in case you forgot. Thank you.

I modded the code to work.. And have been out of town.. I will try to send you what I made to get it working its been running since a week after I asked. I frankly completely forgot about it. On December 7, 2023 1:41:05 PM PST, Chris @.> wrote: I wanted to circle back after trying this out, but now so much time has passed, and it's still sitting on my to-do list. Figured I'd belatedly circle back and say thanks for the response. It's more than enough to play around with / build upon. -- Reply to this email directly or view it on GitHub: #11 (comment) You are receiving this because you authored the thread. Message ID: @.>

MitchellHicks commented 10 months ago

You had Perfect timing... I am sitting in the office... going back out of town... Sorry this is not in DIFF format... Again this is to send airTag information to nextcloud Phonetrack app https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/home Use the Session Token you get from PhoneTrack I didn't make the URL for the server or the Session Token Variables... sorry.. I had to add a check for change or it spams the server with 1,000 of the same datapoint.

It's not Perfect but it's been running for 6mths.

Copyright of changes are under whatever license the existing code is.

import argparse import time import os import curses import requests import urllib.request from subprocess import check_call as shell_cmd from datetime import datetime from tabulate import tabulate from urllib.parse import urlencode from urllib.request import urlopen from lib.constants import JSON_LAYER_SEPARATOR from lib.constants import FINDMY_FILES from lib.constants import NAME_SEPARATOR from lib.constants import JSON_LAYER_SEPARATOR from lib.constants import NULL_STR from lib.constants import TIME_FORMAT from lib.constants import DATE_FORMAT from lib.log_manager import LogManager

def parse_args(): parser = argparse.ArgumentParser( description='Record Apple findmy history for Apple devices.') parser.add_argument( '--refresh', type=int, action='store', default=100, help='Refresh interval (ms).') parser.add_argument( '--name_keys', type=str, action='append', default=['name', 'serialNumber'], help='Keys used to construct the filename for each device.') parser.add_argument( '--store_keys', type=str, action='append', default=['name', 'batteryLevel', 'batteryStatus', 'batteryLevel', f'location{JSON_LAYER_SEPARATOR}timeStamp', f'location{JSON_LAYER_SEPARATOR}latitude', f'location{JSON_LAYER_SEPARATOR}longitude', f'location{JSON_LAYER_SEPARATOR}verticalAccuracy', f'location{JSON_LAYER_SEPARATOR}horizontalAccuracy', f'location{JSON_LAYER_SEPARATOR}altitude', f'location{JSON_LAYER_SEPARATOR}positionType', f'location{JSON_LAYER_SEPARATOR}floorLevel', f'location{JSON_LAYER_SEPARATOR}isInaccurate', f'location{JSON_LAYER_SEPARATOR}isOld', f'location{JSON_LAYER_SEPARATOR}locationFinished', 'id', 'deviceDiscoveryId', 'baUUID', 'serialNumber', 'identifier', 'prsId', 'deviceModel', 'modelDisplayName', 'deviceDisplayName'], help='Keys to log.') parser.add_argument( '--timestamp_key', type=str, action='store', default=f'location{JSON_LAYER_SEPARATOR}timeStamp', help='The key of timestamp in findmy JSON') parser.add_argument( '--log_folder', type=str, action='store', default='log', help='The path of log folder.') parser.add_argument( '--no_date_folder', action='store_true', help='By default, the logs of each day will be saved in a separated ' 'folder. Use this option to turn it off.') parser.add_argument( '--server_url', type=str, action='store', default='https://[Replace with nextcloud Server]/apps/phonetrack/logGet/[Replace with phonetrack Session Token]/', help='The URL of the server to which the data is to be sent') args = parser.parse_args()

return args

def send_to_server(server_url, log): """ Send the log data to a server via HTTP GET request using query string """ if log['serialNumber'] is None: log['serialNumber']=log['name']

server_url = f"https://[Replace with nextcloud

Server]/apps/phonetrack/logGet/[Replace with phonetrack Session Token]/{log['serialNumber']}" # replace with your server URL query_string = { "lat": log[f'location{JSON_LAYER_SEPARATOR}latitude'], "lon": log[f'location{JSON_LAYER_SEPARATOR}longitude'], "alt": log[f'location{JSON_LAYER_SEPARATOR}altitude'], "acc": log[f'location{JSON_LAYER_SEPARATOR}horizontalAccuracy'], "bat": log["batteryLevel"], "sat": log[f'location{JSON_LAYER_SEPARATOR}verticalAccuracy'], "speed": log[f'location{JSON_LAYER_SEPARATOR}positionType'], "bearing": "bearing", "timestamp": log[f'location{JSON_LAYER_SEPARATOR}timeStamp'],

add more key-value pairs as necessary

}

query_params = urlencode(query_string)
req = urllib.request.Request(
    f"{server_url}?{query_params}",
    data=None,
    headers={
        'User-Agent': '{log[name]}'
    }
)

response=""
try:
    response = urllib.request.urlopen(req)
    #print(f"{server_url}?{query_params}")
    #print(f"{server_url}?{query_params}")
    #print(f"Data sent to server with response code: {response.code}")
    return response
except:
    #print(f"{server_url}?{query_params}")
    #print("Failed to send data to server")
    return response

def main(stdscr): stdscr.clear() args = parse_args() log_manager = LogManager( findmy_files=[os.path.expanduser(f) for f in FINDMY_FILES], store_keys=args.store_keys, timestamp_key=args.timestamp_key, log_folder=args.log_folder, name_keys=args.name_keys, name_separator=NAME_SEPARATOR, json_layer_separator=JSON_LAYER_SEPARATOR, null_str=NULL_STR, date_format=DATE_FORMAT, no_date_folder=args.no_date_folder) server_url = args.server_url prev_timestamps = {}

while True:
    log_manager.refresh_log()
    latest_log, log_cnt = log_manager.get_latest_log()

    table = []
    for name, log in latest_log.items():
        latest_time = log[args.timestamp_key]
        if isinstance(latest_time, int) or isinstance(latest_time, float):
            latest_time = datetime.fromtimestamp(
                float(latest_time) / 1000.)
            latest_time = latest_time.strftime(TIME_FORMAT)

        # Check if the timestamp has changed since the last update
        if prev_timestamps.get(name) != latest_time:
            result = send_to_server(server_url, log)
            prev_timestamps[name] = latest_time

        table.append([name, latest_time, log_cnt[name]])

    table = tabulate(
        table,
        headers=['Name', 'Last update', 'Log count'],
        tablefmt="github")

    stdscr.erase()
    try:
        stdscr.addstr(
            0, 0, f'Current time: {datetime.now().strftime(TIME_FORMAT)}')
        stdscr.addstr(1, 0, table)
    except:
        pass
    stdscr.refresh()

    time.sleep(float(args.refresh) / 1000)

if name == "main": try: shell_cmd("open -gja /System/Applications/FindMy.app", shell=True) except:

Maybe Apple changed the name or the dir of the app?

    pass
curses.wrapper(main)

Mitch

On Sat, Dec 23, 2023 at 3:10 PM DShakar @.***> wrote:

Hey Mitchel, I would love to have the modded code, I just thought I would reply to remind you in case you forgot. Thank you.

I modded the code to work.. And have been out of town.. I will try to send you what I made to get it working its been running since a week after I asked. I frankly completely forgot about it. … <#m-7143903286148740668> On December 7, 2023 1:41:05 PM PST, Chris @.> wrote: I wanted to circle back after trying this out, but now so much time has passed, and it's still sitting on my to-do list. Figured I'd belatedly circle back and say thanks for the response. It's more than enough to play around with / build upon. -- Reply to this email directly or view it on GitHub: #11 (comment) https://github.com/fjxmlzn/FindMyHistory/issues/11#issuecomment-1846153647 You are receiving this because you authored the thread. Message ID: @.>

— Reply to this email directly, view it on GitHub https://github.com/fjxmlzn/FindMyHistory/issues/11#issuecomment-1868386475, or unsubscribe https://github.com/notifications/unsubscribe-auth/APBEZBWW2SYXWTRGINAIVL3YK5QERAVCNFSM6AAAAAAWPFHB2GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRYGM4DMNBXGU . You are receiving this because you authored the thread.Message ID: @.***>

MitchellHicks commented 10 months ago

Just remembered... There is no auto correcting of AIR tag names. You can create an air tag name that will be invalid i don't have an example since I renamed all 20 airtags we use rather than fix the code to autocorrect...

Be sure to add that to the notes!! or normalize(that's the word I was looking for) all the names before submitting to PhoneTrack!

Thanks for the work, I hope others can use this...

also sometimes it gets strange CORDS and well it is what it is... lol

Mitch

On Sat, Dec 23, 2023 at 4:42 PM Mitch Hicks @.***> wrote:

You had Perfect timing... I am sitting in the office... going back out of town... Sorry this is not in DIFF format... Again this is to send airTag information to nextcloud Phonetrack app https://gitlab.com/eneiluj/phonetrack-oc/-/wikis/home Use the Session Token you get from PhoneTrack I didn't make the URL for the server or the Session Token Variables... sorry.. I had to add a check for change or it spams the server with 1,000 of the same datapoint.

It's not Perfect but it's been running for 6mths.

Copyright of changes are under whatever license the existing code is.

import argparse import time import os import curses import requests import urllib.request from subprocess import check_call as shell_cmd from datetime import datetime from tabulate import tabulate from urllib.parse import urlencode from urllib.request import urlopen from lib.constants import JSON_LAYER_SEPARATOR from lib.constants import FINDMY_FILES from lib.constants import NAME_SEPARATOR from lib.constants import JSON_LAYER_SEPARATOR from lib.constants import NULL_STR from lib.constants import TIME_FORMAT from lib.constants import DATE_FORMAT from lib.log_manager import LogManager

def parse_args(): parser = argparse.ArgumentParser( description='Record Apple findmy history for Apple devices.') parser.add_argument( '--refresh', type=int, action='store', default=100, help='Refresh interval (ms).') parser.add_argument( '--name_keys', type=str, action='append', default=['name', 'serialNumber'], help='Keys used to construct the filename for each device.') parser.add_argument( '--store_keys', type=str, action='append', default=['name', 'batteryLevel', 'batteryStatus', 'batteryLevel', f'location{JSON_LAYER_SEPARATOR}timeStamp', f'location{JSON_LAYER_SEPARATOR}latitude', f'location{JSON_LAYER_SEPARATOR}longitude', f'location{JSON_LAYER_SEPARATOR}verticalAccuracy', f'location{JSON_LAYER_SEPARATOR}horizontalAccuracy', f'location{JSON_LAYER_SEPARATOR}altitude', f'location{JSON_LAYER_SEPARATOR}positionType', f'location{JSON_LAYER_SEPARATOR}floorLevel', f'location{JSON_LAYER_SEPARATOR}isInaccurate', f'location{JSON_LAYER_SEPARATOR}isOld', f'location{JSON_LAYER_SEPARATOR}locationFinished', 'id', 'deviceDiscoveryId', 'baUUID', 'serialNumber', 'identifier', 'prsId', 'deviceModel', 'modelDisplayName', 'deviceDisplayName'], help='Keys to log.') parser.add_argument( '--timestamp_key', type=str, action='store', default=f'location{JSON_LAYER_SEPARATOR}timeStamp', help='The key of timestamp in findmy JSON') parser.add_argument( '--log_folder', type=str, action='store', default='log', help='The path of log folder.') parser.add_argument( '--no_date_folder', action='store_true', help='By default, the logs of each day will be saved in a separated ' 'folder. Use this option to turn it off.') parser.add_argument( '--server_url', type=str, action='store', default='https://[Replace with nextcloud Server]/apps/phonetrack/logGet/[Replace with phonetrack Session Token]/', help='The URL of the server to which the data is to be sent') args = parser.parse_args()

return args

def send_to_server(server_url, log): """ Send the log data to a server via HTTP GET request using query string """ if log['serialNumber'] is None: log['serialNumber']=log['name']

server_url = f"https://[Replace with nextcloud Server]/apps/phonetrack/logGet/[Replace with phonetrack Session Token]/{log['serialNumber']}"  # replace with your server URL
query_string = {
            "lat": log[f'location{JSON_LAYER_SEPARATOR}latitude'],
            "lon": log[f'location{JSON_LAYER_SEPARATOR}longitude'],
            "alt": log[f'location{JSON_LAYER_SEPARATOR}altitude'],
            "acc": log[f'location{JSON_LAYER_SEPARATOR}horizontalAccuracy'],
            "bat": log["batteryLevel"],
            "sat": log[f'location{JSON_LAYER_SEPARATOR}verticalAccuracy'],
            "speed": log[f'location{JSON_LAYER_SEPARATOR}positionType'],
            "bearing": "bearing",
            "timestamp": log[f'location{JSON_LAYER_SEPARATOR}timeStamp'],
            # add more key-value pairs as necessary
}

query_params = urlencode(query_string)
req = urllib.request.Request(
    f"{server_url}?{query_params}",
    data=None,
    headers={
        'User-Agent': '{log[name]}'
    }
)

response=""
try:
    response = urllib.request.urlopen(req)
    #print(f"{server_url}?{query_params}")
    #print(f"{server_url}?{query_params}")
    #print(f"Data sent to server with response code: {response.code}")
    return response
except:
    #print(f"{server_url}?{query_params}")
    #print("Failed to send data to server")
    return response

def main(stdscr): stdscr.clear() args = parse_args() log_manager = LogManager( findmy_files=[os.path.expanduser(f) for f in FINDMY_FILES], store_keys=args.store_keys, timestamp_key=args.timestamp_key, log_folder=args.log_folder, name_keys=args.name_keys, name_separator=NAME_SEPARATOR, json_layer_separator=JSON_LAYER_SEPARATOR, null_str=NULL_STR, date_format=DATE_FORMAT, no_date_folder=args.no_date_folder) server_url = args.server_url prev_timestamps = {}

while True:
    log_manager.refresh_log()
    latest_log, log_cnt = log_manager.get_latest_log()

    table = []
    for name, log in latest_log.items():
        latest_time = log[args.timestamp_key]
        if isinstance(latest_time, int) or isinstance(latest_time, float):
            latest_time = datetime.fromtimestamp(
                float(latest_time) / 1000.)
            latest_time = latest_time.strftime(TIME_FORMAT)

        # Check if the timestamp has changed since the last update
        if prev_timestamps.get(name) != latest_time:
            result = send_to_server(server_url, log)
            prev_timestamps[name] = latest_time

        table.append([name, latest_time, log_cnt[name]])

    table = tabulate(
        table,
        headers=['Name', 'Last update', 'Log count'],
        tablefmt="github")

    stdscr.erase()
    try:
        stdscr.addstr(
            0, 0, f'Current time: {datetime.now().strftime(TIME_FORMAT)}')
        stdscr.addstr(1, 0, table)
    except:
        pass
    stdscr.refresh()

    time.sleep(float(args.refresh) / 1000)

if name == "main": try: shell_cmd("open -gja /System/Applications/FindMy.app", shell=True) except:

Maybe Apple changed the name or the dir of the app?

    pass
curses.wrapper(main)

Mitch

On Sat, Dec 23, 2023 at 3:10 PM DShakar @.***> wrote:

Hey Mitchel, I would love to have the modded code, I just thought I would reply to remind you in case you forgot. Thank you.

I modded the code to work.. And have been out of town.. I will try to send you what I made to get it working its been running since a week after I asked. I frankly completely forgot about it. … <#m_-2237118060453700791m-7143903286148740668_> On December 7, 2023 1:41:05 PM PST, Chris @.> wrote: I wanted to circle back after trying this out, but now so much time has passed, and it's still sitting on my to-do list. Figured I'd belatedly circle back and say thanks for the response. It's more than enough to play around with / build upon. -- Reply to this email directly or view it on GitHub: #11 (comment) https://github.com/fjxmlzn/FindMyHistory/issues/11#issuecomment-1846153647 You are receiving this because you authored the thread. Message ID: @.>

— Reply to this email directly, view it on GitHub https://github.com/fjxmlzn/FindMyHistory/issues/11#issuecomment-1868386475, or unsubscribe https://github.com/notifications/unsubscribe-auth/APBEZBWW2SYXWTRGINAIVL3YK5QERAVCNFSM6AAAAAAWPFHB2GVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTQNRYGM4DMNBXGU . You are receiving this because you authored the thread.Message ID: @.***>