├── src ├── start.sh └── MqttFindMyPhone.py ├── docker-compose.yml ├── Dockerfile ├── LICENSE ├── README.md └── .gitignore /src/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sudo chmod -R 777 /session 3 | /opt/bin/entry_point.sh & 4 | sleep 10s 5 | python3 /opt/bin/MqttFindMyPhone.py -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | mqttfindmyphone: 4 | image: mqttfindmyphone 5 | build: 6 | context: . 7 | network: host 8 | container_name: mqttfindmyphone 9 | restart: unless-stopped 10 | volumes: 11 | - ./data/session:/session -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM seleniarm/standalone-chromium 2 | RUN sudo apt update 3 | RUN DEBIAN_FRONTEND="noninteractive" sudo apt -y install tzdata 4 | RUN sudo apt install -y python python3 python3-pip build-essential libssl-dev libffi-dev python-dev && sudo rm -rf /var/lib/apt/lists/* 5 | RUN pip3 install selenium paho-mqtt pickle-secure 6 | RUN sudo mkdir /session 7 | RUN sudo chmod 777 /session 8 | COPY src/* /opt/bin/ 9 | RUN sudo chmod +x /opt/bin/start.sh 10 | CMD ["/opt/bin/start.sh"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lukeIam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MqttFindMyPhone 2 | Triggers Android FindMyPhone requests via mqtt as there is no official api. 3 | 4 | ## Warning 5 | This solution requires to store your Google session cookies. If someone steals them, they can be used nearly as your credentials (e.g. access your mails, ...). 6 | 7 | Also, this is not a well build tool, it's just the solution I use for myself. 8 | 9 | ## How to use 10 | - clone the repo 11 | ```bash 12 | git clone https://github.com/lukeIam/MqttFindMyPhone.git 13 | ``` 14 | - build the docker image 15 | ```bash 16 | docker-compose build 17 | ``` 18 | - run the docker container 19 | ``` 20 | docker-compose up -d 21 | ``` 22 | - wait until a message `Not logged in` is published on topic `FindMyPhone/log` 23 | - open a private tab in your browser, login to https://google.com and export the cookies with [EditThisCookie](https://www.editthiscookie.com) 24 | - post the cookie-json as payload to `FindMyPhone/setCookie` (only required once - cookies are stored and refreshed automatically) 25 | - post the device name (exactly as shown [here](https://www.google.com/android/find)) to `FindMyPhone/ring` to trigger a ring (takes a few seconds) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /src/MqttFindMyPhone.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from pickle_secure import pickle_secure 3 | import os 4 | from typing import Dict, List 5 | from selenium import webdriver 6 | from selenium.webdriver.common.by import By 7 | import paho.mqtt.client as mqtt 8 | import json 9 | import re 10 | import time 11 | 12 | MQTT_SERVER = "MQTT_SERVER" 13 | MQTT_PORT = 1883 14 | MQTT_USER = "MQTT_USER" 15 | MQTT_PW = "MQTT_PW" 16 | 17 | 18 | class FindMyPhone: 19 | cookie_path = "/session/cookies.pkl" 20 | url_home: str = "https://www.google.com/" 21 | url_find: str = "https://www.google.com/search?q=find+my+mobile&ie=UTF-8&oe=UTF-8" 22 | 23 | def ring_device(self, ringDevice: str = None, cookies: Dict[str, str] = None): 24 | options = webdriver.ChromeOptions() 25 | options.add_argument("--headless") 26 | options.add_argument("--no-sandbox") 27 | options.add_argument("--disable-gpu") 28 | self.driver = webdriver.Remote("http://127.0.0.1:4444/wd/hub", options=options) 29 | 30 | self.driver.get(self.url_home) 31 | 32 | self.load_cookies() 33 | 34 | if cookies is not None: 35 | self.set_cookies_from_dict(cookies) 36 | 37 | self.driver.refresh() 38 | 39 | if not self.check_logged_in(): 40 | return "Not logged in" 41 | 42 | if ringDevice is None: 43 | return "no ringDevice - nothing todo (session cookie maintained)" 44 | 45 | return self.trigger_ring_device(ringDevice) 46 | 47 | def trigger_ring_device(self, device_name: str): 48 | self.driver.get( 49 | "https://www.google.com/search?q=find+my+mobile&ie=UTF-8&oe=UTF-8" 50 | ) 51 | 52 | btn_for_dropdown = self.driver.find_elements( 53 | By.TAG_NAME, ".gws-action__act-device-label" 54 | ) 55 | if len(btn_for_dropdown) == 0: 56 | return "dropdown not found" 57 | btn_for_dropdown = btn_for_dropdown[0] 58 | btn_for_dropdown.click() 59 | 60 | btn_device_in_dropdown = btn_for_dropdown.find_elements( 61 | By.XPATH, f"//*[contains(text(), '{device_name}')]" 62 | ) 63 | if len(btn_device_in_dropdown) == 0: 64 | return "device not found" 65 | btn_device_in_dropdown = btn_device_in_dropdown[0] 66 | btn_device_in_dropdown.click() 67 | 68 | ring_button = self.driver.find_elements(By.ID, "act-ring-link") 69 | if len(ring_button) == 0: 70 | return "ring button not found" 71 | ring_button = ring_button[0] 72 | time.sleep(1) 73 | ring_button.click() 74 | time.sleep(1) 75 | return f"successfully triggered {device_name}" 76 | 77 | def check_logged_in(self): 78 | self.driver.get(self.url_home) 79 | self.driver.refresh() 80 | 81 | loginBtnQuery = self.driver.find_elements( 82 | By.XPATH, "//a[contains(@href,'ServiceLogin')]" 83 | ) 84 | 85 | if len(loginBtnQuery) == 0: 86 | self.save_cookies() 87 | return True 88 | 89 | return False 90 | 91 | def dispose(self): 92 | self.driver.quit() 93 | 94 | def save_cookies(self): 95 | with open(self.cookie_path, "wb") as f: 96 | pickle_secure.dump( 97 | self.driver.get_cookies(), f, key='asdfjnsadf5&%/"§("§sdfmsdf' 98 | ) 99 | 100 | def load_cookies(self): 101 | if not os.path.isfile(self.cookie_path): 102 | return 103 | with open(self.cookie_path, "rb") as f: 104 | cookies = pickle_secure.load(f, key='asdfjnsadf5&%/"§("§sdfmsdf') 105 | for cookie in cookies: 106 | self.driver.add_cookie(cookie) 107 | 108 | def set_cookies_from_dict(self, cookies): 109 | for key, value in cookies.items(): 110 | self.driver.add_cookie({"name": key, "value": value}) 111 | 112 | 113 | def log(client, message): 114 | print(message) 115 | client.publish("FindMyPhone/log", payload=message) 116 | 117 | 118 | def on_connect(client, userdata, flags, rc): 119 | client.subscribe("FindMyPhone/ring") 120 | client.subscribe("FindMyPhone/setCookie") 121 | 122 | 123 | def on_message(client, userdata, msg): 124 | if msg.topic == "FindMyPhone/ring": 125 | mobile_name = re.sub( 126 | r"[^A-Za-z0-9-_ ]+", "", msg.payload.decode("utf-8").strip() 127 | ) 128 | print(f"request for {mobile_name}") 129 | fmp = FindMyPhone() 130 | res = fmp.ring_device(ringDevice=mobile_name) 131 | fmp.dispose() 132 | log(client, res) 133 | elif msg.topic == "FindMyPhone/setCookie": 134 | print(f"request for setCookie") 135 | try: 136 | cookies = json.loads(msg.payload.decode("utf-8")) 137 | cookies = {item["name"]: item["value"] for item in cookies} 138 | fmp = FindMyPhone() 139 | res = fmp.ring_device(cookies=cookies) 140 | fmp.dispose() 141 | log(client, res) 142 | except Exception as e: 143 | print("error while setCookie: " + repr(e)) 144 | log(client, "error while setCookie: " + repr(e)) 145 | 146 | 147 | def main(): 148 | client = mqtt.Client( 149 | client_id="FindMyPhone", 150 | clean_session=True, 151 | ) 152 | client.on_connect = on_connect 153 | client.on_message = on_message 154 | 155 | client.username_pw_set(username=MQTT_USER, password=MQTT_PW) 156 | client.connect(MQTT_SERVER, MQTT_PORT, 60) 157 | log(client, "Mqtt connected") 158 | client.loop_start() 159 | 160 | while True: 161 | fmp = FindMyPhone() 162 | res = fmp.ring_device() # maintain session 163 | fmp.dispose() 164 | log(client, res) 165 | time.sleep(43200) 166 | 167 | 168 | if __name__ == "__main__": 169 | main() 170 | --------------------------------------------------------------------------------