diff --git a/Auth/fcm_receiver.py b/Auth/fcm_receiver.py index c49f9ef..1f2a103 100644 --- a/Auth/fcm_receiver.py +++ b/Auth/fcm_receiver.py @@ -1,31 +1,28 @@ import asyncio import base64 import binascii - from Auth.firebase_messaging import FcmRegisterConfig, FcmPushClient from Auth.token_cache import set_cached_value, get_cached_value class FcmReceiver: - _instance = None _listening = False - + def __new__(cls, *args, **kwargs): if cls._instance is None: - cls._instance = super(FcmReceiver, cls).__new__(cls, *args, **kwargs) + cls._instance = super(FcmReceiver, cls).__new__(cls) return cls._instance - + def __init__(self): if hasattr(self, '_initialized') and self._initialized: return self._initialized = True - + # Define Firebase project configuration project_id = "google.com:api-project-289722593072" app_id = "1:289722593072:android:3cfcf5bc359f0308" api_key = "AIzaSyD_gko3P392v6how2H7UpdeXQ0v2HLettc" message_sender_id = "289722593072" - fcm_config = FcmRegisterConfig( project_id=project_id, app_id=app_id, @@ -33,84 +30,102 @@ def __init__(self): messaging_sender_id=message_sender_id, bundle_id="com.google.android.apps.adm", ) - self.credentials = get_cached_value('fcm_credentials') self.location_update_callbacks = [] self.pc = FcmPushClient(self._on_notification, fcm_config, self.credentials, self._on_credentials_updated) - - - def register_for_location_updates(self, callback): - + self.listen_task = None + self.timeout_task = None + + def register_for_location_updates(self, callback, timeout_seconds=60): if not self._listening: - asyncio.get_event_loop().run_until_complete(self._register_for_fcm_and_listen()) - + asyncio.get_event_loop().run_until_complete( + self._register_for_fcm_and_listen(timeout_seconds) + ) self.location_update_callbacks.append(callback) - return self.credentials['fcm']['registration']['token'] - - + def stop_listening(self): + if self.timeout_task and not self.timeout_task.done(): + self.timeout_task.cancel() + if self.listen_task and not self.listen_task.done(): + self.listen_task.cancel() asyncio.get_event_loop().run_until_complete(self.pc.stop()) self._listening = False - - + def get_android_id(self): - if self.credentials is None: - return asyncio.get_event_loop().run_until_complete(self._register_for_fcm_and_listen()) - + return asyncio.get_event_loop().run_until_complete( + self._register_for_fcm_and_listen() + ) return self.credentials['gcm']['android_id'] - - + # Define a callback function for handling notifications def _on_notification(self, obj, notification, data_message): - + # Reset the timeout timer when we receive a notification + if self.timeout_task and not self.timeout_task.done(): + self.timeout_task.cancel() + # Check if the payload is present if 'data' in obj and 'com.google.android.apps.adm.FCM_PAYLOAD' in obj['data']: - # Decode the base64 string base64_string = obj['data']['com.google.android.apps.adm.FCM_PAYLOAD'] decoded_bytes = base64.b64decode(base64_string) - - # print("[FCMReceiver] Decoded FMDN Message:", decoded_bytes.hex()) - # Convert to hex string hex_string = binascii.hexlify(decoded_bytes).decode('utf-8') - for callback in self.location_update_callbacks: callback(hex_string) else: print("[FCMReceiver] Payload not found in the notification.") - - + def _on_credentials_updated(self, creds): self.credentials = creds - # Also store to disk set_cached_value('fcm_credentials', self.credentials) print("[FCMReceiver] Credentials updated.") - - + + async def _timeout_handler(self, timeout_seconds): + try: + await asyncio.sleep(timeout_seconds) + print(f"[FCMReceiver] Timed out after {timeout_seconds} seconds") + if self._listening: + await self.pc.stop() + self._listening = False + except asyncio.CancelledError: + # This is normal when a notification is received and the timeout is canceled + pass + async def _register_for_fcm(self): fcm_token = None - # Register or check in with FCM and get the FCM token while fcm_token is None: try: fcm_token = await self.pc.checkin_or_register() except Exception as e: await self.pc.stop() - print("[FCMReceiver] Failed to register with FCM. Retrying...") + print(f"[FCMReceiver] Failed to register with FCM: {str(e)}. Retrying...") await asyncio.sleep(5) - - - async def _register_for_fcm_and_listen(self): + + async def _register_for_fcm_and_listen(self, timeout_seconds=60): await self._register_for_fcm() - await self.pc.start() + + self.listen_task = asyncio.create_task(self.pc.start()) self._listening = True print("[FCMReceiver] Listening for notifications. This can take a few seconds...") - + + # Set up the timeout + if timeout_seconds > 0: + self.timeout_task = asyncio.create_task(self._timeout_handler(timeout_seconds)) if __name__ == "__main__": receiver = FcmReceiver() - print(receiver.get_android_id()) \ No newline at end of file + try: + # Example usage with a 30-second timeout + def on_location_update(hex_data): + print(f"Received location update: {hex_data[:20]}...") + + receiver.register_for_location_updates(on_location_update, timeout_seconds=30) + # Keep the main thread running + asyncio.get_event_loop().run_forever() + except KeyboardInterrupt: + print("Stopping...") + receiver.stop_listening() \ No newline at end of file diff --git a/NovaApi/ExecuteAction/LocateTracker/decrypt_locations.py b/NovaApi/ExecuteAction/LocateTracker/decrypt_locations.py index c0e7033..2a278ab 100644 --- a/NovaApi/ExecuteAction/LocateTracker/decrypt_locations.py +++ b/NovaApi/ExecuteAction/LocateTracker/decrypt_locations.py @@ -129,32 +129,47 @@ def decrypt_location_response_locations(device_update_protobuf): if not location_time_array: print("No locations found.") - return - - for loc in location_time_array: - - if loc.status == Common_pb2.Status.SEMANTIC: - print(f"Semantic Location: {loc.name}") - - else: - proto_loc = DeviceUpdate_pb2.Location() - proto_loc.ParseFromString(loc.decrypted_location) - - latitude = proto_loc.latitude / 1e7 - longitude = proto_loc.longitude / 1e7 - altitude = proto_loc.altitude - - print(f"Latitude: {latitude}") - print(f"Longitude: {longitude}") - print(f"Altitude: {altitude}") - print(f"Google Maps Link: {create_google_maps_link(latitude, longitude)}") - + return None + + # Return data from the most recent location + loc = location_time_array[0] + + if loc.status == Common_pb2.Status.SEMANTIC: + print(f"Semantic Location: {loc.name}") + location_data = { + 'semantic_location': loc.name, + 'timestamp': datetime.datetime.fromtimestamp(loc.time).strftime('%Y-%m-%d %H:%M:%S'), + 'status': loc.status, + 'is_own_report': loc.is_own_report + } + else: + proto_loc = DeviceUpdate_pb2.Location() + proto_loc.ParseFromString(loc.decrypted_location) + + latitude = proto_loc.latitude / 1e7 + longitude = proto_loc.longitude / 1e7 + altitude = proto_loc.altitude + + print(f"Latitude: {latitude}") + print(f"Longitude: {longitude}") + print(f"Altitude: {altitude}") + print(f"Google Maps Link: {create_google_maps_link(latitude, longitude)}") print(f"Time: {datetime.datetime.fromtimestamp(loc.time).strftime('%Y-%m-%d %H:%M:%S')}") print(f"Status: {loc.status}") print(f"Is Own Report: {loc.is_own_report}") print("-" * 40) - pass + location_data = { + 'latitude': latitude, + 'longitude': longitude, + 'altitude': altitude, + 'accuracy': loc.accuracy, + 'timestamp': datetime.datetime.fromtimestamp(loc.time).strftime('%Y-%m-%d %H:%M:%S'), + 'status': loc.status, + 'is_own_report': loc.is_own_report + } + + return location_data if __name__ == '__main__': diff --git a/NovaApi/ExecuteAction/LocateTracker/location_request.py b/NovaApi/ExecuteAction/LocateTracker/location_request.py index 9dec8a5..01cce3f 100644 --- a/NovaApi/ExecuteAction/LocateTracker/location_request.py +++ b/NovaApi/ExecuteAction/LocateTracker/location_request.py @@ -53,7 +53,8 @@ def handle_location_response(response): while result is None: asyncio.get_event_loop().run_until_complete(asyncio.sleep(0.1)) - decrypt_location_response_locations(result) + locations = decrypt_location_response_locations(result) + return locations if __name__ == '__main__': get_location_data_for_device(get_example_data("sample_canonic_device_id"), "Test") \ No newline at end of file diff --git a/NovaApi/ListDevices/nbe_list_devices.py b/NovaApi/ListDevices/nbe_list_devices.py index 7590f31..478ce0f 100644 --- a/NovaApi/ListDevices/nbe_list_devices.py +++ b/NovaApi/ListDevices/nbe_list_devices.py @@ -1,8 +1,3 @@ -# -# GoogleFindMyTools - A set of tools to interact with the Google Find My API -# Copyright © 2024 Leon Böttger. All rights reserved. -# - import binascii from NovaApi.ExecuteAction.LocateTracker.location_request import get_location_data_for_device from NovaApi.nova_request import nova_request @@ -15,10 +10,8 @@ def request_device_list(): - hex_payload = create_device_list_request() result = nova_request(NOVA_LIST_DEVICS_API_SCOPE, hex_payload) - return result @@ -56,20 +49,12 @@ def list_devices(): print("") print("The following trackers are available:") + # Anstatt Eingabeaufforderung, jedes Gerät automatisch durchlaufen for idx, (device_name, canonic_id) in enumerate(canonic_ids, start=1): print(f"{idx}. {device_name}: {canonic_id}") - selected_value = input("\nIf you want to see locations of a tracker, type the number of the tracker and press 'Enter'.\nIf you want to register a new ESP32- or Zephyr-based tracker, type 'r' and press 'Enter': ") - - if selected_value == 'r': - print("Loading...") - register_esp32() - else: - selected_idx = int(selected_value) - 1 - selected_device_name = canonic_ids[selected_idx][0] - selected_canonic_id = canonic_ids[selected_idx][1] - - get_location_data_for_device(selected_canonic_id, selected_device_name) + # Automatisch den Standort jedes Geräts abfragen + get_location_data_for_device(canonic_id, device_name) if __name__ == '__main__': diff --git a/README.md b/README.md index 488eab1..addbf45 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,491 @@ -# GoogleFindMyTools +# GoogleFindMyTools (Raspberry) Home Assistant -This repository includes some useful tools that reimplement parts of Google's Find My Device Network. Note that the code of this repo is still very experimental. +Dies ist ein fork of https://github.com/endeavour/GoogleFindMyTools-homeassistant -### What's possible? -Currently, it is possible to query Find My Device trackers and Android devices, read out their E2EE keys, and decrypt encrypted locations sent from the Find My Device network. You can also send register your own ESP32- or Zephyr-based trackers, as described below. +Dieser Fork von GoogleFindMyTools ist für den Betrieb auf Raspberry OS gedacht, da es dort ansonsten Probleme mit Chromeium und dem login in Chrome gibt. Die Kommunikation findet über Mqqt zu Home Assistant (Mqqt Brocker) statt, welches auf einem anderen Gerät läuft. -### How to use -- Clone this repository: `git clone` or download the ZIP file -- Change into the directory: `cd GoogleFindMyTools` -- Optional: Create venv: `python -m venv venv` -- Optional: Activate venv: `venv\Scripts\activate` (Windows) or `source venv/bin/activate` (Linux & macOS) -- Install all required packages: `pip install -r requirements.txt` -- Install the latest version of Google Chrome: https://www.google.com/chrome/ -- Start the program by running [main.py](main.py): `python main.py` or `python3 main.py` +Da Google Find my Device entweder einen Standort in Koordinatenform oder einen String "home" bzw. "zuhause", wurde die publish_mqqt.py angepasst. Falls google nun den string zuhause sendet, ersetzt der Raspbbery diesen durch Koordinaten für die Home Zone. +Der Aufruf zum aktualisieren des Standortes erfolgt über Home Assisant via mqtt. In diesem sind die Kooardinaten für die Homezone (Koordinaten + Radius) enthalten. -### Authentication +Die Home Zone (Koordinaten + Umkreis) sind nötig, da Home Assistant immer einen Status zur Übermittlung der Attribute (Koordinaten) benötigt. Vorher hat die publisch_mqtt.py immer unkown als Status gesendet und die Koordinaten als Attribute. Die folge war, das home assistent den tracker bei jeder Standort aktualisierung auf unkown gesetzt hat und dann die Attribute ausliest, um dann wieder "home" als status zu setzten. Mit dieser Änderung wird direkt der richtige status an home Assistent gesendet. -On the first run, an authentication sequence is executed, which requires a computer with access to Google Chrome. +Da der Chrome Browser auf dem Raspberry beim "requstest url was not found on this server" meldet, kann man sich dort nicht einloggen. Man muss daher GoogleFindMyTools auf einem Windows PC installieren, sich einloggen und die secrets.json mit den Zugangsdaten von dem PC auf den Raspberry kopieren. Daher ist die Anleitung in drei Schritte aufgeteilt: Installation auf dem Windows PC, installation auf dem Raspberry OS und anschließend die Mqqt Verbindung zu Home Assistant. -The authentication results are stored in `Auth/secrets.json`. If you intend to run this tool on a headless machine, you can just copy this file to avoid having to use Chrome. +## Installation Windows PC: -### Known Issues -- "Your encryption data is locked on your device" is shown if you have never set up Find My Device on an Android device. Solution: Login with your Google Account on an Android device, go to Settings > Google > All Services > Find My Device > Find your offline devices > enable "With network in all areas" or "With network in high-traffic areas only". If "Find your offline devices" is not shown in Settings, you will need to download the Find My Device app from Google's Play Store, and pair a real Find My Device tracker with your device to force-enable the Find My Device network. -- No support for trackers using the P-256 curve and 32-Byte advertisements. Regular trackers don't seem to use this curve at all - I can only confirm that it is used with Sony's WH1000XM5 headphones. -- No support for the authentication process on ARM Linux -- Please also consider the issues listed in the [README in the ESP32Firmware folder](ESP32Firmware/README.md) if you want to register custom trackers. +git installieren: https://git-scm.com/download/win
+Python installieren: https://www.python.org/downloads/windows/ -### Firmware for custom ESP32-based trackers -If you want to use an ESP32 as a custom Find My Device tracker, you can find the firmware in the folder ESP32Firmware. To register a new tracker, run main.py and press 'r' if you are asked to. Afterward, follow the instructions on-screen. +Im Chromebrowser mit dem Nutzkonto einloggen. Wichtig: hiermit ist nicht die website von google.com gemeint, sondern das Chrome Desktop Programm! -For more information, check the [README in the ESP32Firmware folder](ESP32Firmware/README.md). +PowerShell als Admin ausführen und GoogleFindMyTools ** von leonboe1 ** installieren -### Firmware for custom Zephyr-based trackers -If you want to use a Zephyr-supported BLE device (e.g. nRF51/52) as a custom Find My Device tracker, you can find the firmware in the folder ZephyrFirmware. To register a new tracker, run main.py and press 'r' if you are asked to. Afterward, follow the instructions on-screen. -For more information, check the [README in the ZephyrFirmware folder](ZephyrFirmware/README.md). +``` +git clone https://github.com/leonboe1/GoogleFindMyTools +``` +``` +cd GoogleFindMyTools +``` +``` +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process +``` +``` +python -m venv venv +``` +falls dass nicht geht ```& "C:\Users\[USER]\AppData\Local\Programs\Python\Python313\python.exe" -m venv venv``` +hier bei muss [USER] durch den PC User ersetzt werden bzw. wo auch immer wurde +``` +venv\Scripts\activate +``` +alternativ ```.\venv\Scripts\Activate.ps1``` -### iOS App -You can also use my [iOS App](https://testflight.apple.com/join/rGqa2mTe) to access your Find My Device trackers on the go. +``` +cd GoogleFindMyTools +``` +``` +pip install -r requirements.txt +``` +``` +python main.py +``` +
+
+
+
+Es könnte nun der Fehler "undetected_chromedriver" kommen. In diesem Fall muss der Chromedriver separat installiert werden + +Zuerst muss man die Chrome Version herausfinden: Öffne Chrome und gib chrome://settings/help in die Adresszeile ein. Notiere die Version, z.B. 114.0.5735.199 +Nun muss man den passenden ChromeDriver herunterladen: https://googlechromelabs.github.io/chrome-for-testing/. Wenn die erste Zahl z.B. 144 der Versionnummer übereinstimmt, reicht das. + +Entpacke die datei chromedriver.exe nach C:\Tools\chromedriver\
+Nun müssen wir noch den dateipfad anpassen, damit GoogleFindMyTools weiß wo dieser liegt. + +Gehe unter C:\WINDOWS\system32\GoogleFindMyTools\chrome_driver.py und öffne die Datei als Admin + + +wir müssen folgenden Block +``` +def create_driver(): + """Create a Chrome WebDriver with undetected_chromedriver.""" + + try: + chrome_options = get_options() + driver = uc.Chrome(options=chrome_options) + print("[ChromeDriver] Installed and browser started.") + return driver + except Exception: + print("[ChromeDriver] Default ChromeDriver creation failed. Trying alternative paths...") + + chrome_path = find_chrome() + if chrome_path: + chrome_options = get_options() + chrome_options.binary_location = chrome_path + try: + driver = uc.Chrome(options=chrome_options) + print(f"[ChromeDriver] ChromeDriver started using {chrome_path}") + return driver + except Exception as e: + print(f"[ChromeDriver] ChromeDriver failed using path {chrome_path}: {e}") + else: + print("[ChromeDriver] No Chrome executable found in known paths.") + + raise Exception( + "[ChromeDriver] Failed to install ChromeDriver. A current version of Chrome was not detected on your system.\n" + "If you know that Chrome is installed, update Chrome to the latest version. If the script is still not working, " + "set the path to your Chrome executable manually inside the script." + ) +``` + +durch diesen ersetzten + +``` +def create_driver(): + """Create a Chrome WebDriver with undetected_chromedriver.""" + + try: + chrome_options = get_options() + driver = uc.Chrome(options=chrome_options) + print("[ChromeDriver] Installed and browser started.") + return driver + except Exception: + print("[ChromeDriver] Default ChromeDriver creation failed. Trying alternative paths...") + + chrome_path = find_chrome() + if chrome_path: + chrome_options = get_options() + #chrome_options.binary_location = chrome_path + chrome_options.debugger_address = "127.0.0.1:9222" + try: + #driver = uc.Chrome(options=chrome_options) + driver = uc.Chrome( + options=chrome_options, + driver_executable_path="C:\\Tools\\chromedriver\\chromedriver.exe", + use_subprocess=False + ) + print(f"[ChromeDriver] ChromeDriver started using {chrome_path}") + return driver + except Exception as e: + print(f"[ChromeDriver] ChromeDriver failed using path {chrome_path}: {e}") + else: + print("[ChromeDriver] No Chrome executable found in known paths.") + + raise Exception( + "[ChromeDriver] Failed to install ChromeDriver. A current version of Chrome was not detected on your system.\n" + "If you know that Chrome is installed, update Chrome to the latest version. If the script is still not working, " + "set the path to your Chrome executable manually inside the script." + ) +``` + +Hierdurch wird unser neuer Chromedriver verwendet und Chrome in Debug modus geöffnet. Melde dich dort im Chorme mit deinem Google Profil noch einmal an (wichtig: nicht die Website sondern im Chrome). Schließe alle Browser Fenster + +gebe nun +``` +& "C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222 --user-data-dir="C:\ChromeDebugTemp" +``` +in powershell ein und führe +``` +python main.py +``` +noch einmal aus. Nun sollte es funktionieren. + + +PowerShell lassen wir geöffnet und richten nun den Raspberry ein. + + + +## Installation Raspberry: +Für einen Aufbau habe ich einen Raspberry 2 b verwendet und dort Raspberry OS Bookworm verwendet. Die Verbindung kann man mit Putty (Port 22) von Windows zum Raspberry herstellen. Anschließend mit User und Passwort anmelden. + +``` +sudo apt install chromium-browser +``` +``` +sudo apt install chromium-chromedriver +``` +Es es klappt kann man sich über die UI im Chromebrowser anmelden. Falls nicht "requstest url was not found on this server", muss man die secrets.json von windows später zum Raspberry kopieren + +``` +sudo apt install systemd-networkd-wait-online +``` +``` +sudo systemctl enable systemd-networkd-wait-online.service +``` + +Installieren wir nun GoogleFindMyTools +``` +git clone https://github.com/xHecktor/GoogleFindMyTools-homeassistant.git ~/GoogleFindMyTools +``` +``` +cd ~/GoogleFindMyTools +``` +``` +python3 -m venv venv +``` +``` +source venv/bin/activate +``` +``` +pip install -r requirements.txt +``` +``` +python3 main.py +``` +oder ```python main.py``` + + +hier wird nun ein Fehler wegen der Zugangsdaten von Chrome Browser kommen. Daher gehen wir wieder in Powershell und übertragen nun die Zugangsdaten von Windows zum Raspberry. +In PowerShell folgenes eintrippen: +``` +scp Auth\secrets.json admin@raspberrypi.local:~/GoogleFindMyTools/Auth/ +``` +ggf. muss hier admin durch den User und raspberry durch den Gerätenamen im oben link ersetzt werden. + + +

+nun müssen wir die Daten vom Mqtt Brocker eintragen. Daher wieder in Putty die publish_mqtt.py auf dem Raspberry öffenen +``` +nano publish_mqtt.py +``` +und folgende Felder anpassten: + +``` +MQTT_BROKER = "192.168.1.100" # Ändere die IP zu der von Home Assistant +MQTT_PORT = 1883 +MQTT_USERNAME = "DeinMqttUser" # Ändere einen Usernamen +MQTT_PASSWORD = "DeinMqttPasswort" # Ändere dein Passwort +``` +Drücke STG+X, dann Y und Enter + +


+Auch die Zugangsdaten vom LIstener anpassen, um die Trigger von Home Assistant zu empfangen: +``` +nano mqtt_listener.py +``` +hier müssen auch wieder die Zugangsdaten des Mqtt Brockers angepasst werden + +``` +MQTT_BROKER = "192.168.100" +MQTT_PORT = 1883 +MQTT_TOPIC = "googlefindmytools/trigger/update" +MQTT_USER = "DeinMqqtUser" +MQTT_PASS = "DeinMqttPasswort" + +``` + +Drücke STG+X, dann Y und Enter + +``` +chmod +x mqtt_listener.py +``` + + +


+Abschließend müssen wir noch den Listener Service erstellen +``` +sudo nano /etc/systemd/system/mqtt_listener.service +``` +hier muss folgendes hineinkopiert werden (Achtung User Verzeichnis anpassen, hier admin): +``` + +[Unit] +Description=MQTT Listener for Google Find My Tools +After=network-online.target +Wants=network-online.target +StartLimitIntervalSec=60 +StartLimitBurst=5 + + +[Service] +#Type=simple +User=admin +WorkingDirectory=/home/admin/GoogleFindMyTools +Environment="PATH=/home/admin/GoogleFindMyTools/venv/bin" +#ExecStart=/home/admin/GoogleFindMyTools/venv/bin/python mqtt_listener.py +ExecStart=/home/admin/GoogleFindMyTools/venv/bin/python /home/admin/GoogleFindMyTools/mqtt_listener.py + +# stderr ins Nichts leiten, stdout bleibt im Journal +#StandardError=null +StandardOutput=journal +StandardError=journal + +# 🧹 Alte Subprozesse (z. B. nbe_list_devices.py) aufräumen +ExecStopPost=/usr/bin/pkill -f nbe_list_devices.py + +# → Watchdog einschalten +Type=notify +WatchdogSec=30 +NotifyAccess=all + +# Fallback‑Restart, falls das Skript wirklich abstürzt +Restart=on-failure +RestartSec=5 + + +[Install] +WantedBy=multi-user.target +``` +Drücke STG+X, dann Y und Enter +``` +sudo systemctl daemon-reexec +``` +``` +sudo systemctl daemon-reload +``` +``` +sudo systemctl enable mqtt_listener.service +``` +``` +sudo systemctl start mqtt_listener.service +``` +Nun müssen wir noch einen Watchdog erstellen. Dies ist leider nur eine behälfsmäßige Lösung. Ich habe die Erfahrung gemacht, dass sich der update Service gerne aufhängt. Der Watchdoog schaut, ob der Updateprozess innnerhalb von 400 Sekunden fertig gemeldet hat. Ist es nicht der Fall, killt alles und startet es neu. 400s habe ich deshalb eingestellt, da Home Assistant alle 5 min einen Trigger schickt. Wenn Home Assistant seltener die Trigger sendet, sollte auch der Watchdog angepasst werden. + +Um den Watchdog zu starten +``` +sudo apt install watchdog +``` +``` +sudo systemctl enable watchdog +``` +``` +sudo systemctl start watchdog +``` +``` +sudo nano /etc/watchdog.conf +``` +Aktiviere folgende Zeilen, indem du die Raute davor entfernst. Falls nicht vorhanden füge diese einfach hinzu. +``` +watchdog-device = /dev/watchdog +max-load-1 = 24 +temperature-device = /sys/class/thermal/thermal_zone0/temp +max-temperature = 75000 + +``` + + + +listener Service testen: +``` +sudo systemctl status mqtt_listener.service +``` +``` +journalctl -u mqtt_listener.service -f +``` +



+wenn man sich später die kommunikation mit HA anschauen möchte: +``` +cd /home/admin +source ~/GoogleFindMyTools/venv/bin/activate +journalctl -u mqtt_listener.service -f + +``` +Str + c zum beenden + +wenn man später die kommunikation mit HA sehen möchte, einfach ein zweites Puttyfenster aufmachen und folgendes eingeben: +user ip und password müssen natürlich die von deinem Broker (Home Assistant) sein +``` +mosquitto_sub -h 192.168.1.100 -u DeinMqttUser -P DeinMqttPassword -v -t "homeassistant/#" +``` +Str + c zum beenden + + +# Home Assistant +Abschnließend müssen wir noch den Broker in Home Assistant installieren + +In der configuration.yaml +``` +mqtt: +``` +hinzufügen + +Als nächstes müssen wir einen neuen User zu HA hinzufügen.
+Einstellungen/Personen/Benutzer hinzufügen
+Dieser DeinMqqtUser und DeinMqqtPasswort muss mit dem übereinstimmen, was wir oben im Raspberry bereits hinterlegt haben.
+ + +Nun installieren wir den Mqqt Broker:
+In Home Assistant Einstellungen/ Geräte&Dienste Integration hinzufügen drücken und nach Mqtt suchen und installieren und das offizielle Mqqt auswählen (nicht das manuelle mit den Benutzerdetails)
+ +Nun richten wir den Broker ein:
+Einstellungen/ Geräte&Dienste / MQTT

+ +Dort gibt es nun einen Integrationseintrag: "Mosquitto Mqtt Broker". Dort gehen wir auf die drei Punkte und wählen "Neu konfigueren" aus.
+Folgendes geben wir ein:
+Server: core-mosquitto
+Port: 1883
+Benutzername: DeinMqqtUser den du beim Raspberry verwendet hast in der mqtt_listener.py und in der publish_mqtt.py
+Passwort: DeinMqttPasswort das du beim Raspberry verwendet hast in der mqtt_listener.py und in der publish_mqtt.py
+ +



+Nun erstellen wir den Aufruf zum Orten der Google Tags
+Einstellungen/Automatisierungen&Szenen/Skripte/ hier auf den Button "+Skript erstellen" klicken und "Neues Skipt erstellen im Dialog auswählen
+nun sind wir im Editor der GUI geführt ist. Um es uns leichter zu machen, klick oben rechts aud ie drei Punkt und wähle "in YAML bearbeiten" aus
+ + +Hier kannst du die Koordinaten und den Umkreis von deinem Zuhause definieren. Wenn nun der Google Tag anstatt von Koordinaten nur "Zuhause" meldet, wird dieses in Koordinaten umgewandelt, damit Home Assisant den Tracker anzeigen kann.
+Kopiere das Skript in den Editor + +``` +alias: Google Tracker aktualisieren +sequence: + - data: + topic: googlefindmytools/trigger/update + payload: "{ \"lat_home\": 31.8909428, \"lon_home\": 7.1704316, \"home_radius\": 500 }" + action: mqtt.publish +``` +speichern und ausführen. Die Google Tags sollten nun in Home Assistan angezeigt werden (Einstellungen/Geräte und Dienste/MQTT). + +Zuletzt legen wir noch eine Automatisierung ab, um den Standort alle 5 min zu aktualisieren:
+Einstellungen/Automatisierungen&Szenen/ auf den Button "+Automatisierung erstellen" klicken und "Neue Automation erstellen" auswählen
+nun sind wir im Editor der GUI geführt ist. Um es uns leichter zu machen, klick oben rechts aud ie drei Punkt und wähle "in YAML bearbeiten" aus
+Kopiere das Skript in den Editor
+``` +alias: Google_Airtag +description: "" +triggers: + - trigger: time_pattern + minutes: /5 +conditions: [] +actions: + - action: script.update_google_locations + metadata: {} + data: {} +mode: single +``` +speichern + + + +**So fertig sind wir** + + +**kleines Extra:** +Wer es noch brauchen kann hier sind drei Karten für das dashboard
+Einfach in das Dashboard gehen, eine irgendeine neue Karte hinzufügen und auf "im Code Editor anzeigen" gehen.
+Anschleißend folgenden code hinein kopieren und die Trackernamen (Airtag_1) anpassen
+ +Hier eine Karte +``` +type: map +entities: + - device_tracker.Airtag_1 + - device_tracker.Airtag_2 + - device_tracker.Airtag_3 +default_zoom: 16 +hours_to_show: 1 +theme_mode: auto +``` + +Hier die Personen +``` +type: tile +features_position: bottom +vertical: true +entity: device_tracker.Airtag_1 +state_content: + - state + - last_changed + - semantic_location +grid_options: + rows: 2 + columns: 6 +``` + +Hier als Markdown +``` +type: markdown +title: Google Airtag Status +content: > + **Status:** {{ states('device_tracker.Airtag_1') }} + + **Letzter Wechsel:** {% if states.device_tracker.Airtag_1.last_changed %}{{ + as_timestamp(states.device_tracker.Airtag_1.last_changed)| + timestamp_custom('%Y-%m-%d %H:%M:%S') }}{% else %}– + + {% endif %} + + **Semantic Location:** {{ state_attr('device_tracker.Airtag_1', + 'semantic_location') or '–' }} + + **Letztes GPS‑Update:** {{ state_attr('device_tracker.Airtag_1', + 'last_updated') or '–' }} +grid_options: + columns: 9 + rows: auto +``` +Ein Button zum manuellen aktualisieren der Tracker +```show_name: true +show_icon: true +type: button +entity: automation.google_airtag +tap_action: + action: perform-action + perform_action: automation.trigger + target: + entity_id: automation.google_airtag + data: + skip_condition: true +``` + + +Viel Spaß damit diff --git a/chrome_driver.py b/chrome_driver.py index 903198e..88a9f9d 100644 --- a/chrome_driver.py +++ b/chrome_driver.py @@ -1,86 +1,21 @@ -# -# GoogleFindMyTools - A set of tools to interact with the Google Find My API -# Copyright © 2024 Leon Böttger. All rights reserved. -# - -import undetected_chromedriver as uc -import os -import shutil -import platform - -def find_chrome(): - """Find Chrome executable using known paths and system commands.""" - possiblePaths = [ - r"C:\Program Files\Google\Chrome\Application\chrome.exe", - r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", - r"C:\ProgramData\chocolatey\bin\chrome.exe", - r"C:\Users\%USERNAME%\AppData\Local\Google\Chrome\Application\chrome.exe", - "/usr/bin/google-chrome", - "/usr/local/bin/google-chrome", - "/opt/google/chrome/chrome", - "/snap/bin/chromium", - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" - ] - - # Check predefined paths - for path in possiblePaths: - if os.path.exists(path): - return path - - # Use system command to find Chrome - try: - if platform.system() == "Windows": - chrome_path = shutil.which("chrome") - else: - chrome_path = shutil.which("google-chrome") or shutil.which("chromium") - if chrome_path: - return chrome_path - except Exception as e: - print(f"[ChromeDriver] Error while searching system paths: {e}") - - return None - - -def get_options(): - chrome_options = uc.ChromeOptions() - chrome_options.add_argument("--start-maximized") - chrome_options.add_argument("--disable-extensions") - chrome_options.add_argument("--disable-gpu") - chrome_options.add_argument("--no-sandbox") - - return chrome_options - +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service def create_driver(): - """Create a Chrome WebDriver with undetected_chromedriver.""" - - try: - chrome_options = get_options() - driver = uc.Chrome(options=chrome_options) - print("[ChromeDriver] Installed and browser started.") - return driver - except Exception: - print("[ChromeDriver] Default ChromeDriver creation failed. Trying alternative paths...") - - chrome_path = find_chrome() - if chrome_path: - chrome_options = get_options() - chrome_options.binary_location = chrome_path - try: - driver = uc.Chrome(options=chrome_options) - print(f"[ChromeDriver] ChromeDriver started using {chrome_path}") - return driver - except Exception as e: - print(f"[ChromeDriver] ChromeDriver failed using path {chrome_path}: {e}") - else: - print("[ChromeDriver] No Chrome executable found in known paths.") - - raise Exception( - "[ChromeDriver] Failed to install ChromeDriver. A current version of Chrome was not detected on your system.\n" - "If you know that Chrome is installed, update Chrome to the latest version. If the script is still not working, " - "set the path to your Chrome executable manually inside the script." - ) - - -if __name__ == '__main__': - create_driver() \ No newline at end of file + chrome_options = Options() + chrome_options.add_argument("--no-sandbox") + chrome_options.add_argument("--disable-dev-shm-usage") + chrome_options.add_argument("--headless=new") + chrome_options.add_argument("--disable-gpu") + chrome_options.add_argument("--window-size=1920,1080") + + # Manueller Pfad zu chromedriver (überprüft mit `which chromedriver`) + service = Service(executable_path="/usr/bin/chromedriver") + + try: + driver = webdriver.Chrome(service=service, options=chrome_options) + print("[ChromeDriver] ChromeDriver gestartet.") + return driver + except Exception as e: + raise Exception(f"[ChromeDriver] Fehler beim Start von ChromeDriver: {e}") diff --git a/mqtt_listener.py b/mqtt_listener.py new file mode 100644 index 0000000..c816a7e --- /dev/null +++ b/mqtt_listener.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 15 16:08:02 2025 + +@author: Jan +""" + +from systemd.daemon import notify +import threading +import time +import sys + + +import json +import time +from math import radians, cos, sin, asin, sqrt +import paho.mqtt.client as mqtt +from NovaApi.ListDevices.nbe_list_devices import request_device_list +from NovaApi.ExecuteAction.LocateTracker.location_request import get_location_data_for_device +from ProtoDecoders.decoder import parse_device_list_protobuf, get_canonic_ids + +# MQTT Configuration +MQTT_BROKER = "192.168.1.100" # Change this to your MQTT broker address +MQTT_PORT = 1883 +MQTT_USERNAME = "DeinMqttUser" # Set your MQTT username if required +MQTT_PASSWORD = "DeinMqttPassword" # Set your MQTT password if required + +# Home zone defaults +lat_home = 0 # placeholder until config arrives +lon_home = 0 +home_radius = 0 +config_received = False +last_full_update = 0.0 + +MQTT_CLIENT_ID = "google_find_my_publisher" + +# Home Assistant MQTT Discovery prefixes +DISCOVERY_PREFIX = "homeassistant" +DEVICE_PREFIX = "google_find_my" + +current_home_config = { + "lat_home": lat_home, + "lon_home": lon_home, + "home_radius": home_radius +} + +# MQTT Callbacks and helpers + +def _thread_excepthook(args): + """Beende den Prozess, sobald irgendein Thread eine unbehandelte Exception wirft.""" + print(f"⚠️ Uncaught thread exception: {args.exc_value!r} – exiting.") + sys.exit(1) +threading.excepthook = _thread_excepthook + + +def calculate_distance(lat1, lon1, lat2, lon2): + """Berechnet Entfernung zwischen zwei GPS-Koordinaten in Metern.""" + lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + c = 2 * asin(sqrt(a)) + return 6371000 * c # Erdradius + + +def on_any_message(client, userdata, msg): + print(f"ANY MSG: {msg.topic} → {msg.payload.decode()}") + + +def on_connect(client, userdata, flags, result_code, properties=None): + print("🔌 Connected to MQTT, rc=", result_code) + client.subscribe([ + ("googlefindmytools/config", 0), + ("googlefindmytools/trigger/update", 0), + ]) + print("Current home config:", current_home_config) + print("✅ Subscribed to config & trigger/update") + +def on_disconnect(*args, **kwargs): + print("⚠️ MQTT disconnected—will reconnect…") + +def on_config_message(client, userdata, msg): + global config_received + print(f"[Config] Topic: {msg.topic}, Payload: {msg.payload.decode()}") + try: + payload = json.loads(msg.payload.decode()) + current_home_config.update({ + "lat_home": float(payload.get("lat_home", current_home_config["lat_home"])), + "lon_home": float(payload.get("lon_home", current_home_config["lon_home"])), + "home_radius": int(payload.get("home_radius", current_home_config["home_radius"])) + }) + config_received = True + print("✅ Updated home zone:", current_home_config) + except Exception as e: + print("❌ Error parsing config:", e) + + +def publish_device_config(client, device_name, canonic_id): + base = f"{DISCOVERY_PREFIX}/device_tracker/{DEVICE_PREFIX}_{canonic_id}" + cfg = { + "unique_id": f"{DEVICE_PREFIX}_{canonic_id}", + "state_topic": f"{base}/state", + "json_attributes_topic": f"{base}/attributes", + "source_type": "gps", + "device": {"identifiers": [f"{DEVICE_PREFIX}_{canonic_id}"], + "name": device_name, + "model": "Google Find My Device", + "manufacturer": "Google"} + } + client.publish(f"{base}/config", json.dumps(cfg), retain=True) + + +def publish_device_state(client, name, cid, loc): + lat = loc.get("latitude") + lon = loc.get("longitude") + sem = loc.get("semantic_location") + if lat is not None and lon is not None: + dist = calculate_distance(current_home_config["lat_home"], + current_home_config["lon_home"], lat, lon) + state = "home" if dist < current_home_config["home_radius"] else "not_home" + elif sem: + state = "home" if sem.lower() == "zuhause" else sem + lat, lon = current_home_config["lat_home"], current_home_config["lon_home"] + else: + state = "unknown" + + attrs = { + "latitude": lat, + "longitude": lon, + "altitude": loc.get("altitude"), + "gps_accuracy": loc.get("accuracy"), + "source_type": "gps" if lat is not None else "semantic", + "last_updated": loc.get("timestamp"), + "semantic_location": sem + } + + topic_s = f"homeassistant/device_tracker/google_find_my_{cid}/state" + topic_a = f"homeassistant/device_tracker/google_find_my_{cid}/attributes" + client.publish(topic_s, state, retain=True) + client.publish(topic_a, json.dumps(attrs), retain=True) + + +def run_full_update(client): + """Ruft Liste ab und published Config+State für alle Geräte.""" + global last_full_update + print("▶️ Triggered run_full_update at", time.strftime("%Y-%m-%d %H:%M:%S")) + try: + hexdata = request_device_list() + devices = parse_device_list_protobuf(hexdata) + for name, cid in get_canonic_ids(devices): + publish_device_config(client, name, cid) + loc = get_location_data_for_device(cid, name) + publish_device_state(client, name, cid, loc or {}) + print("✅ Full update complete") + last_full_update = time.time() + except Exception as e: + print("❌ run_full_update error:", e) + + +def main(): + global last_full_update + + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, MQTT_CLIENT_ID) + client.on_connect = on_connect + client.on_message = on_any_message + client.message_callback_add("googlefindmytools/config", on_config_message) + #client.message_callback_add("googlefindmytools/trigger/update", lambda c,u,m: run_full_update(c)) + client.message_callback_add( + "googlefindmytools/trigger/update", + lambda c, u, m: ( + print("📨 Received trigger/update payload:", m.payload.decode()), + run_full_update(c) + ) + ) + + + client.reconnect_delay_set(min_delay=1, max_delay=60) + client.on_disconnect = on_disconnect + + # Auth + client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + + print("🔌 Connecting to MQTT…") + client.connect(MQTT_BROKER, MQTT_PORT) + + # Start loop + client.loop_start() + + notify("READY=1") + + last_full_update = time.time() + + # Watchdog-Ping alle 10 Sekunden + def _wd_pinger(): + INTERVAL = 30 # Sekunde(n) zwischen Pings + THRESHOLD = 400 # wenn seit 60 s kein Full‑Update lief, kein Ping mehr + while True: + now = time.time() + if now - last_full_update < THRESHOLD: + notify("WATCHDOG=1") + else: + print("⚠️ Dienst scheint nicht mehr gesund (kein Full‑Update seit", + int(now - last_full_update), "s). Watchdog darf eingreifen.") + break + time.sleep(INTERVAL) + + + threading.Thread(target=_wd_pinger, daemon=True).start() + + + # wait for config if needed + + t0 = time.time() + while not config_received and time.time() - t0 < 5: + time.sleep(0.1) + if not config_received: + print("⚠️ No config received, using defaults.") + + # initial update + run_full_update(client) + + # stay in loop for triggers + client.loop_forever() + + +if __name__ == '__main__': + main() + diff --git a/publish_mqtt.py b/publish_mqtt.py new file mode 100644 index 0000000..4b72997 --- /dev/null +++ b/publish_mqtt.py @@ -0,0 +1,278 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jul 15 16:08:02 2025 + +@author: Jan +""" + +import json +import time +from typing import Dict, Any + +#from publish_mqtt import current_home_config +from math import radians, cos, sin, asin, sqrt +import paho.mqtt.client as mqtt +from NovaApi.ListDevices.nbe_list_devices import request_device_list +from NovaApi.ExecuteAction.LocateTracker.location_request import get_location_data_for_device +from ProtoDecoders.decoder import parse_device_list_protobuf, get_canonic_ids + +# MQTT Configuration +MQTT_BROKER = "192.168.1.100" # Change this to your MQTT broker address +MQTT_PORT = 1883 +MQTT_USERNAME = "DeinMqttUser" # Set your MQTT username if required +MQTT_PASSWORD = "DeinMqttPassword" # Set your MQTT password if required +lat_home = 0 #48.8909528 # DEIN Zuhause-Breitengrad +lon_home = 0 #9.1904316 # DEIN Zuhause-Längengrad +home_cycle = 0 #200 # Umkreis der Homezone in [m] +config_received = False + +MQTT_CLIENT_ID = "google_find_my_publisher" + +current_home_config = { + "lat_home": lat_home, + "lon_home": lon_home, + "home_radius": home_cycle +} + +# Home Assistant MQTT Discovery +DISCOVERY_PREFIX = "homeassistant" +DEVICE_PREFIX = "google_find_my" + +def on_any_message(client, userdata, msg): + print(f"ANY MSG: {msg.topic} → {msg.payload.decode()}") + + + +def on_connect(client, userdata, flags, result_code, properties): + """Callback when connected to MQTT broker""" + print("===> on_config_message aufgerufen!") + print(f"Connected to MQTT broker with result code {result_code}") + client.subscribe("googlefindmytools/config") + print(current_home_config) + + + +def calculate_distance(lat1, lon1, lat2, lon2): + """Berechnet Entfernung zwischen zwei GPS-Koordinaten in Metern.""" + lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2]) + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2 + c = 2 * asin(sqrt(a)) + return 6371000 * c # Erdradius in Metern + + +def on_config_message(client, userdata, msg): + global current_home_config, config_received + print(f"[MQTT] Nachricht empfangen auf Topic: {msg.topic}") + print(f"[MQTT] Payload: {msg.payload.decode()}") + try: + payload = json.loads(msg.payload.decode()) + lat = float(payload.get("lat_home", current_home_config["lat_home"])) + lon = float(payload.get("lon_home", current_home_config["lon_home"])) + radius = int(payload.get("home_radius", current_home_config["home_radius"])) + + current_home_config["lat_home"] = lat + current_home_config["lon_home"] = lon + current_home_config["home_radius"] = radius + + config_received = True # Markiere, dass Konfiguration eingetroffen ist + + print(f"[Config] Neue Home-Zone: lat={lat}, lon={lon}, radius={radius} m") + except Exception as e: + print(f"[Config] Fehler beim Verarbeiten der Konfiguration: {e}") + + + + + +def publish_device_config(client: mqtt.Client, device_name: str, canonic_id: str) -> None: + """Publish Home Assistant MQTT discovery configuration for a device""" + base_topic = f"{DISCOVERY_PREFIX}/device_tracker/{DEVICE_PREFIX}_{canonic_id}" + + # Device configuration for Home Assistant + config = { + "unique_id": f"{DEVICE_PREFIX}_{canonic_id}", + "state_topic": f"{base_topic}/state", + "json_attributes_topic": f"{base_topic}/attributes", + "source_type": "gps", + "device": { + "identifiers": [f"{DEVICE_PREFIX}_{canonic_id}"], + "name": device_name, + "model": "Google Find My Device", + "manufacturer": "Google" + } + } + print(f"{base_topic}/config") + # Publish discovery config + r = client.publish(f"{base_topic}/config", json.dumps(config), retain=True) + return r + +def publish_device_state(client, name, cid, location_data): + # 1) Hole Home‑Zone + lat_home = current_home_config["lat_home"] + lon_home = current_home_config["lon_home"] + home_radius = current_home_config["home_radius"] + + # 2) Versuche GPS‑Koordinaten + lat = location_data.get("latitude") + lon = location_data.get("longitude") + sem = location_data.get("semantic_location") + + # 3) State‑Ermittlung + if lat is not None and lon is not None: + dist = calculate_distance(lat_home, lon_home, lat, lon) + state = "home" if dist < home_radius else "not_home" + + elif sem: + # Fallback: semantischer Raum → immer Home‑Koordinaten + lat, lon = lat_home, lon_home + if sem.lower() == "zuhause": + state = "home" + else: + state = sem # z.B. "Arbeitszimmer" + print(f"[Fallback] Semantische Position erkannt: '{sem}' → Fallback-Koordinaten werden verwendet.") + + else: + # keinerlei Info + state = "unknown" + + # 4) Attribute‑Payload + attrs = { + "latitude": lat, + "longitude": lon, + "altitude": location_data.get("altitude"), + "gps_accuracy": location_data.get("accuracy"), + "source_type": "gps" if location_data.get("latitude") is not None else "semantic", + "last_updated": location_data.get("timestamp"), + "semantic_location": sem + } + + # 5) Publish + topic_state = f"homeassistant/device_tracker/google_find_my_{cid}/state" + topic_attr = f"homeassistant/device_tracker/google_find_my_{cid}/attributes" + + client.publish(topic_state, state, retain=True) + client.publish(topic_attr, json.dumps(attrs), retain=True) + +def main(): + # Initialize MQTT client + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, MQTT_CLIENT_ID) + #client.subscribe("googlefindmytools/config") + client.on_connect = on_connect + client.on_message = on_any_message + + client.message_callback_add("googlefindmytools/config", on_config_message) + #client.subscribe("googlefindmytools/config") + + + if MQTT_USERNAME and MQTT_PASSWORD: + client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + + try: + print("Starte Verbindung zu MQTT...") + client.connect(MQTT_BROKER, MQTT_PORT) + client.loop_start() + print("→ MQTT Verbindung gestartet") + print("Warte auf MQTT-Konfiguration... (max 5 Sekunden)") + timeout = 5 + waited = 0 + while not config_received and waited < timeout: + time.sleep(0.1) + waited += 0.1 + if int(waited * 10) % 10 == 0: # jede volle Sekunde + print(f" ...warte seit {int(waited)}s") + if config_received: + print("Konfiguration empfangen.") + else: + print("Keine Konfiguration empfangen – verwende Default-Werte.") + + + print("Loading devices...") + result_hex = request_device_list() + device_list = parse_device_list_protobuf(result_hex) + canonic_ids = get_canonic_ids(device_list) + + print(f"Found {len(canonic_ids)} devices") + + # Publish discovery config and state for each device + for device_name, canonic_id in canonic_ids: + print("\n" + "=" * 60) + print(f"Processing device: {device_name}") + print("=" * 60) + + + # Publish discovery config (optional – funktioniert nicht zwingend jedes Mal) + try: + if client.is_connected(): + msg_info = publish_device_config(client, device_name, canonic_id) + msg_info.wait_for_publish() + else: + print(f"[Discovery] MQTT nicht verbunden – Discovery für {device_name} übersprungen.") + except Exception as e: + print(f"[Discovery] Error publishing config for {device_name}: {e}") + + # Get and publish location data + try: + location_data = get_location_data_for_device(canonic_id, device_name) + if location_data: + if client.is_connected(): + msg_info = publish_device_state(client, device_name, canonic_id, location_data) + msg_info.wait_for_publish() + print(f"Published data for {device_name}") + else: + raise Exception("MQTT client is not connected during state publish") + else: + raise Exception("Keine Standortdaten vorhanden") + except Exception as e: + print(f"[Error] Fehler beim Verarbeiten von {device_name}: {e}") + + # Fallback auf unknown, wenn keine Daten gepublisht werden konnten + try: + if client.is_connected(): + fallback_info = client.publish( + f"homeassistant/device_tracker/{DEVICE_PREFIX}_{canonic_id}/state", + payload="unknown", + retain=True + ) + fallback_info.wait_for_publish() + print(f"[Fallback] Unknown-Status für {device_name} veröffentlicht.") + else: + print(f"[Fallback] MQTT nicht verbunden – konnte 'unknown' für {device_name} nicht setzen.") + except Exception as retry_e: + print(f"[Fallback] Fehler beim Setzen von 'unknown' für {device_name}: {retry_e}") + + print("-" * 60) + + + print("\nAll devices have been published to MQTT") + print("Devices will now be discoverable in Home Assistant") + print("You may need to restart Home Assistant or trigger device discovery") + + + + finally: + print("→ MQTT Loop stoppen...") + client.loop_stop() # Stoppt die MQTT-Loop + print("→ MQTT Verbindung trennen...") + client.disconnect() # Trennt die Verbindung zum Broker + + # Falls ein FCM Listener läuft, stoppen (nur wenn du darauf Zugriff hast) + try: + if 'fcm_client' in locals(): # Überprüfen, ob fcm_client existiert + fcm_client.stop() # Falls du eine Instanz gespeichert hast + print("→ FCM Listener gestoppt.") + else: + print("→ Kein FCM Listener gefunden.") + except AttributeError as e: + print(f"[Error] Fehler beim Stoppen des FCM Listeners: {e}") + except Exception as e: + print(f"[Unexpected Error] Ein unerwarteter Fehler ist aufgetreten: {e}") + + + + + +if __name__ == '__main__': + main() + diff --git a/requirements.txt b/requirements.txt index 8ddccc4..5adb8e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -undetected-chromedriver>=3.5.5 +#undetected-chromedriver>=3.5.5 selenium>=4.27.1 gpsoauth>=1.1.1 requests>=2.32.3 @@ -14,4 +14,5 @@ httpx>=0.28.0 h2>=4.1.0 setuptools>=75.6.0 aiohttp>=3.11.8 -http_ece>=1.1.0 \ No newline at end of file +http_ece>=1.1.0 +paho_mqtt