"""
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]))