Open mitchellrj opened 5 years ago
I just got my eufy RoboVac 15C today and was planning on helping out with the efforts to get it to show up in HomeBridge. I assume there is a vast amount of information that can be shared between HomeAssistant/HomeBridge (I know the existing Eufy bulb HomeBridge support came from a porting of what HA uses).
I was wondering how you managed to get your local key. I can grab the device key from the API along with my user id but even emulating Android I can't find the local key (I use iOS but to your point #1 it looks like it makes sense I can't see it in the logs).
EDIT: I just saw your post here and that was exactly what I needed. I had been logcat-ing my emulator but either missed that or needed the -e 'tuya.m.my.group.device.list'
to make it work. THANK YOU!
The Home Assistant Eufy Page shows how to get the local key (access token):
https://www.home-assistant.io/components/eufy/
curl -H "Content-Type: application/json" -d '{"client_id":"eufyhome-app", "client_Secret":"GQCpr9dSp3uQpsOMgJ4xQ", "email":"[replace by your eufy login email", "password":"[replace by your eufy login password]"}' https://home-api.eufylife.com/v1/user/email/login
curl -H token:[replace by access_token from previous command] -H category:Home https://home-api.eufylife.com/v1/device/list/devices-and-groups
"access_token" can be get from the first command "id" is "device":{"id" from 2nd command "type" is "product_code" from 2nd command
@abalakov That is correct for older devices, but does not apply to the 30C line. The local key for these newer devices is not held by the Eufy API.
is there a working implementation for the 30c? Have also had trouble finding the devices IP on my router, not sure what range of mac addresses they are using either
@jimmyeao Have you tried using the deviceId instead of the IP? I have it working with my 30C https://github.com/joshstrange/eufy-robovac but ended up not using it as it a little buggy (probably my code) and at the end of the day I just use mine on a schedule or use the Eufy App as it's decent.
Unfortunately I haven’t got an android device to be able to do this. The app works fine, was just a nice to have really :)
Jimmy White
From: Josh Strange notifications@github.com Sent: Monday, August 19, 2019 3:41:54 PM To: mitchellrj/eufy_robovac eufy_robovac@noreply.github.com Cc: Jimmy White jimmy@deviousweb.com; Mention mention@noreply.github.com Subject: Re: [mitchellrj/eufy_robovac] Discover local key from Eufy login details (#1)
@jimmyeaohttps://github.com/jimmyeao Have you tried using the deviceId instead of the IP? I have it working with my 30C https://github.com/joshstrange/eufy-robovac but ended up not using it as it a little buggy (probably my code) and at the end of the day I just use mine on a schedule or use the Eufy App as it's decent.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/mitchellrj/eufy_robovac/issues/1?email_source=notifications&email_token=ABHVAB7SEV7BSI35TZAVZ3LQFKWLFA5CNFSM4HHQTUVKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4TFMQI#issuecomment-522606145, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ABHVAB2LID3WCD5T6M7ZCRLQFKWLFANCNFSM4HHQTUVA.
@jimmyeao I don't either but I used an android emulator on my mac to get the keys/ids.
Ah, hadn’t thought of that!
Jimmy White
From: Josh Strange notifications@github.com Sent: Monday, August 19, 2019 4:47:48 PM To: mitchellrj/eufy_robovac eufy_robovac@noreply.github.com Cc: Jimmy White jimmy@deviousweb.com; Mention mention@noreply.github.com Subject: Re: [mitchellrj/eufy_robovac] Discover local key from Eufy login details (#1)
@jimmyeaohttps://github.com/jimmyeao I don't either but I used an android emulator on my mac to get the keys/ids.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHubhttps://github.com/mitchellrj/eufy_robovac/issues/1?email_source=notifications&email_token=ABHVABZYETK55DSC7S4NOATQFK6CJA5CNFSM4HHQTUVKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD4TM3GY#issuecomment-522636699, or mute the threadhttps://github.com/notifications/unsubscribe-auth/ABHVABYLVY46D6QD3NLDIUTQFK6CJANCNFSM4HHQTUVA.
Hey, @joshstrange @mitchellrj is it still accurate to use ADB to get the localKey
and the deviceId
? I've been struggling to get them.
Running the command will output:
$ adb logcat -e 'tuya.m.my.group.device.list'
--------- beginning of system
--------- beginning of main
Hi, @abrahamduran
$ adb shell logcat -e 'tuya.m.my.group.device.list'
Worked for me.
@resain did you use a real device or BlueStacks? 🤔
BlueStacks on Windows 10
My 15c MAX is running 1.1.3 and it no longer puts out the LocalID and DevID - Im hoping https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md can get me that info
@mitchellrj - any luck with the signing process? would be great to get the localkey online
Figured I'd chip in too. Same as @robbrad I can't get LocalKey nor DevId from logcat when opening the Eufy Home app.
Also tried emptying the storage of the app, and having adb open from a fresh phone reboot. Still nothing.
I am on a Galaxy S7, running Android 7.0
The thing is that I got these 2 values in the past, around the time when this repo was first created but ended up not using them. Now I wanted to give it another try, but they don't show up using ADB.
Happy to help in any way I can should you need extra info @mitchellrj
Ok. I was able to get the local_key, by downloading an older apk version of EufyHome (2.3.2).
+1 for @bmtKIA6 's solution.
Worked for me to get the localKey
Briefly tested my RoboVac 30C (https://github.com/mitchellrj/eufy_robovac/issues/1#issuecomment-599771209) with the recently acquired localKey
and both start
and back to home
commands worked fine. I'll test it more thoroughly later but great work so far!
Thank you so much @mitchellrj and @bmtKIA6 :clap:
Was anyone able to use this method with a RoboVac 11c? I can sniff the deviceID, but not the localKey. Is that the current name of the string?
I am using BlueStacks on Mac and the EufyHome 2.3.2.
Not entirely the same issue, but I'm a Home Assistant user who similarly cannot fetch local_code
for my Eufy smart bulbs. It looks like something has shifted from their end and we are at a loss on where the new endpoint might be.
I used the official guide with auto-discovery turned on and off.
Does anyone have this working for a 30c that can share the config yaml settings?
Sorry everyone, my 30C has now broken and I won't be replacing it any time soon. I can't help with resolving this issue at this point.
Happy to update documentation or accept fixes though if people find them.
Does anyone have this working for a 30c that can share the config yaml settings?
@jimmyeao I do. I'll share the code as soon as I have my hass setup back online (just moved houses and I have all my stuff in boxes)
@jimmyeao
My config card: https://gist.github.com/pabsi/d5a9d4211a4c0da5bb88c89a0311dd0d
Had edge mode cleaning by doing this: https://github.com/home-assistant/core/pull/25483
I don't have this vacuum anymore but happy to help in any way I can.
Regards,
found an easier waty to get the token, thanks to an old version of the app:
https://github.com/lorenzofattori/robovac30c_homeassistant hope to save some time to someone else :)
I've spent the past few hours implementing API clients for both the Eufy API as well as the Tuya API, complete with the request signature hash, copious amounts of MD5 hashing (were the original developers paid per amount of MD5()
calls?) and cargo-cult, poorly-implemented cryptography.
The file below can be ran as a script and takes Eufy Home credentials - email and password as positional command-line arguments. You'll need to pip install requests cryptography
beforehand. It'll print out all devices detected on the underlying Tuya account including device ID and local key.
The clients can of course be used to also send commands to the Tuya cloud and obtain cloud control of the Robovac (or other devices) if desired.
License is public domain. I'd love for this to be integrated into Home Assistant at some point with an easy GUI-based set up process that just takes Eufy credentials and autodiscovers every device on there. I may clean it up later and restructure the code, put it in a dedicated repo, etc but in the meantime I figured I'd post it here if anyone finds this useful.
Enjoy!
Edit: the code is now on GitLab.
I've spent the past few hours implementing API clients for both the Eufy API as well as the Tuya API, complete with the request signature hash, copious amounts of MD5 hashing (were the original developers paid per amount of
MD5()
calls?) and cargo-cult, poorly-implemented cryptography.The file below can be ran as a script and takes Eufy Home credentials - email and password as positional command-line arguments. You'll need to
pip install requests cryptography
beforehand. It'll print out all devices detected on the underlying Tuya account including device ID and local key.The clients can of course be used to also send commands to the Tuya cloud and obtain cloud control of the Robovac (or other devices) if desired.
License is public domain. I'd love for this to be integrated into Home Assistant at some point with an easy GUI-based set up process that just takes Eufy credentials and autodiscovers every device on there. I may clean it up later and restructure the code, put it in a dedicated repo, etc but in the meantime I figured I'd post it here if anyone finds this useful.
Enjoy!
import hmac import json import math import random import string import sys import time import uuid from hashlib import md5, sha256 from urllib.parse import urljoin import requests from cryptography.hazmat.backends.openssl import backend as openssl_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes def unpadded_rsa(key_exponent: int, key_n: int, plaintext: bytes) -> bytes: # RSA with no padding, as per https://github.com/pyca/cryptography/issues/2735#issuecomment-276356841 keylength = math.ceil(key_n.bit_length() / 8) input_nr = int.from_bytes(plaintext, byteorder="big") crypted_nr = pow(input_nr, key_exponent, key_n) return crypted_nr.to_bytes(keylength, byteorder="big") # from Android Eufy App EUFY_CLIENT_ID = "eufyhome-app" EUFY_CLIENT_SECRET = "GQCpr9dSp3uQpsOMgJ4xQ" # from capturing traffic EUFY_BASE_URL = "https://home-api.eufylife.com/v1/" PLATFORM = "sdk_gphone64_arm64" LANGUAGE = "en" TIMEZONE = "Europe/London" DEFAULT_EUFY_HEADERS = { "User-Agent": "EufyHome-Android-2.4.0", "timezone": TIMEZONE, "category": "Home", # the original app has a bug/oversight where sends these as blank on the initial request # we replicate that to avoid detection as much as possible # when we login these will be overridden by the real values "token": "", "uid": "", "openudid": PLATFORM, "clientType": "2", "language": LANGUAGE, "country": "US", "Accept-Encoding": "gzip", } class EufyHomeSession: base_url = None email = None password = None def __init__(self, email, password): self.session = requests.session() self.session.headers = DEFAULT_EUFY_HEADERS.copy() self.base_url = EUFY_BASE_URL self.email = email self.password = password def url(self, url): return urljoin(self.base_url, url) def login(self, email, password): resp = self.session.post( self.url("user/email/login"), json={ "client_Secret": EUFY_CLIENT_SECRET, "client_id": EUFY_CLIENT_ID, "email": email, "password": password, }, ) resp.raise_for_status() data = resp.json() access_token = data["access_token"] user_id = data["user_info"]["id"] new_base_url = data["user_info"]["request_host"] self.session.headers["uid"] = user_id self.session.headers["token"] = access_token self.base_url = new_base_url def _request(self, *args, **kwargs): if not self.session.headers["token"] or not self.session.headers["uid"]: self.login(self.email, self.password) resp = self.session.request(*args, **kwargs) resp.raise_for_status() return resp.json() def get_devices(self): # Not actually needed; Eufy uses a single Tuya account per user so we can lookup the devices there # however you may still want to consult this for human-readable product names # as Tuya only returns generic product IDs and presumably has no knowledge of user-facing names return self._request("GET", self.url("device/v2")).get("devices", []) def get_user_info(self): return self._request("GET", self.url("user/info"))["user_info"] # from Eufy Home Android app TUYA_CLIENT_ID = "yx5v9uc3ef9wg3v9atje" DEFAULT_TUYA_QUERY_PARAMS = { "appVersion": "2.4.0", "deviceId": "", "platform": PLATFORM, "clientId": TUYA_CLIENT_ID, "lang": LANGUAGE, "osSystem": "12", "os": "Android", "timeZoneId": TIMEZONE, "ttid": "android", "et": "0.0.1", "sdkVersion": "3.0.8cAnker", } # that for other regions you may need to change this? # TODO: is this somehow returned by the Eufy API, and if so, can we set this automatically? TUYA_ENDPOINT = "https://a1.tuyaeu.com/api.json" DEFAULT_TUYA_HEADERS = {"User-Agent": "TY-UA=APP/Android/2.4.0/SDK/null"} # from decompiling the Android app SIGNATURE_RELEVANT_PARAMETERS = { "a", "v", "lat", "lon", "lang", "deviceId", "appVersion", "ttid", "isH5", "h5Token", "os", "clientId", "postData", "time", "requestId", "et", "n4h5", "sid", "sp", } def shuffled_md5(value: str) -> str: # shuffling the hash reminds me of https://security.stackexchange.com/a/25588 # from https://github.com/TuyaAPI/cloud/blob/9b108f4d347c81c3fd6d73f3a2bb08a646a2f6e1/index.js#L99 _hash = md5(value.encode("utf-8")).hexdigest() return _hash[8:16] + _hash[0:8] + _hash[24:32] + _hash[16:24] # Eufy Home "TUYA_SMART_SECRET" Android app metadata value APPSECRET = "s8x78u7xwymasd9kqa7a73pjhxqsedaj" # obtained using instructions at https://github.com/nalajcie/tuya-sign-hacking BMP_SECRET = "cepev5pfnhua4dkqkdpmnrdxx378mpjr" # turns out this is not used by the Eufy app but FYI this value is from the Eufy Home app in case it's useful # APP_CERT_HASH = "A4:0D:A8:0A:59:D1:70:CA:A9:50:CF:15:C1:8C:45:4D:47:A3:9B:26:98:9D:8B:64:0E:CD:74:5B:A7:1B:F5:DC" # hmac_key = f'{APP_CERT_HASH}_{BMP_SECRET}_{APPSECRET}'.encode('utf-8') # turns out this app just uses "A" instead of the app's certificate hash EUFY_HMAC_KEY = f"A_{BMP_SECRET}_{APPSECRET}".encode("utf-8") # from https://github.com/mitchellrj/eufy_robovac/issues/1 TUYA_PASSWORD_KEY = bytearray( [36, 78, 109, 138, 86, 172, 135, 145, 36, 67, 45, 139, 108, 188, 162, 196] ) TUYA_PASSWORD_IV = bytearray( [119, 36, 86, 242, 167, 102, 76, 243, 57, 44, 53, 151, 233, 62, 87, 71] ) TUYA_PASSWORD_INNER_CIPHER = Cipher( algorithms.AES(TUYA_PASSWORD_KEY), modes.CBC(TUYA_PASSWORD_IV), backend=openssl_backend, ) class TuyaAPISession: username = None country_code = None session_id = None def __init__(self, username, country_code): self.session = requests.session() self.session.headers = DEFAULT_TUYA_HEADERS.copy() self.default_query_params = DEFAULT_TUYA_QUERY_PARAMS.copy() self.default_query_params[ "deviceId" ] = self.device_id = self.generate_new_device_id() self.username = username self.country_code = country_code @staticmethod def generate_new_device_id(): """ In the Eufy Android app this is generated as follows:
private static String getRemoteDeviceID(Context paramContext) { StringBuilder stringBuilder1 = new StringBuilder(); stringBuilder1.append(Build.BRAND); stringBuilder1.append(Build.MODEL); String str1 = MD5Util.md5AsBase64(stringBuilder1.toString()); StringBuilder stringBuilder2 = new StringBuilder(); stringBuilder2.append(getAndroidId(paramContext)); stringBuilder2.append(getSerialNum()); String str2 = MD5Util.md5AsBase64(stringBuilder2.toString()); StringBuilder stringBuilder3 = new StringBuilder(); stringBuilder3.append(getImei(paramContext)); stringBuilder3.append(getImsi(paramContext)); String str3 = MD5Util.md5AsBase64(stringBuilder3.toString()); StringBuilder stringBuilder4 = new StringBuilder(); stringBuilder4.append(str1.substring(4, 16)); stringBuilder4.append(str2.substring(8, 24)); stringBuilder4.append(str3.substring(16)); return stringBuilder4.toString(); } ``` In short, we should be able to get away with a random 44-char string, though the first 12 characters of the resulting value are sourced from "str1" which is a hash of the device's brand & model, and as such could be predictable and may need to be faked to a popular device's make/model to make detection & blocking harder (as they'd block legitimate devices too). """ expected_length = 44 base64_characters = ( string.ascii_letters + string.digits ) # TODO: is this correct? device_id_dependent_part = ( "8534c8ec0ed0" # from Google Pixel in an Android Virtual Device ) return device_id_dependent_part + "".join( ( random.choice(base64_characters) for _ in range(expected_length - len(device_id_dependent_part)) ) ) @staticmethod def encode_post_data(data: dict) -> str: # Note: the rest of the code relies on empty dicts being encoded as a blank string as opposed to "{}" return json.dumps(data, separators=(",", ":")) if data else "" @staticmethod def get_signature(query_params: dict, encoded_post_data: str): """ Tuya's proprietary request signature algorithm. This relies on HMAC'ing specific query parameters (defined in SIGNATURE_RELEVANT_PARAMETERS) as well as the data, if any. If data is present, it is included in the parameters to be hashed as "postData" and then MD5 & the resulting hash shuffled, where as query parameter values are passed through as-is. The HMAC key is derived from the app secret value "TUYA_SMART_SECRET" in the Android app's metadata as well as a value obfuscated in an innocent-looking bitmap image in the app's assets. There is also provision for the key to include the app's signing certificate, but in the Eufy build it just defaults to "A". """ query_params = query_params.copy() if encoded_post_data: query_params["postData"] = encoded_post_data sorted_pairs = sorted(query_params.items()) filtered_pairs = filter( lambda p: p[0] and p[0] in SIGNATURE_RELEVANT_PARAMETERS, sorted_pairs ) mapped_pairs = map( # postData is pre-emptively hashed (for performance reasons?), everything else is included as-is lambda p: p[0] + "=" + (shuffled_md5(p[1]) if p[0] == "postData" else p[1]), filtered_pairs, ) message = "||".join(mapped_pairs) return hmac.HMAC( key=EUFY_HMAC_KEY, msg=message.encode("utf-8"), digestmod=sha256 ).hexdigest() def _request( self, action: str, version="1.0", data: dict = None, query_params: dict = None, _requires_session=True, ): if not self.session_id and _requires_session: self.acquire_session() current_time = time.time() request_id = uuid.uuid4() extra_query_params = { "time": str(int(current_time)), "requestId": str(request_id), "a": action, "v": version, **(query_params or {}), } query_params = {**self.default_query_params, **extra_query_params} encoded_post_data = self.encode_post_data(data) resp = self.session.post( TUYA_ENDPOINT, params={ **query_params, "sign": self.get_signature(query_params, encoded_post_data), }, # why do they send JSON as a single form-encoded value instead of just putting it directly in the body? # they spent more time implementing the stupid request signature system than actually designing a good API data={"postData": encoded_post_data} if encoded_post_data else None, ) resp.raise_for_status() data = resp.json() return data["result"] def request_token(self, username, country_code): return self._request( action="tuya.m.user.uid.token.create", data={"uid": username, "countryCode": country_code}, _requires_session=False, ) def determine_password(self, username: str): new_uid = username password_uid = new_uid.zfill(16) encryptor = TUYA_PASSWORD_INNER_CIPHER.encryptor() encrypted_uid = encryptor.update(password_uid.encode("utf8")) encrypted_uid += encryptor.finalize() # from looking into the Android app the password now appears to be MD5-hashed return md5(encrypted_uid.hex().upper().encode("utf-8")).hexdigest() def request_session(self, username, country_code): password = self.determine_password(username) token_response = self.request_token(username, country_code) encrypted_password = unpadded_rsa( key_exponent=int(token_response["exponent"]), key_n=int(token_response["publicKey"]), plaintext=password.encode("utf-8"), ) data = { "uid": username, "createGroup": True, "ifencrypt": 1, "passwd": encrypted_password.hex(), "countryCode": country_code, "options": '{"group": 1}', "token": token_response["token"], } session_response = self._request( action="tuya.m.user.uid.password.login.reg", data=data, _requires_session=False, ) return session_response def acquire_session(self): session_response = self.request_session(self.username, self.country_code) self.session_id = self.default_query_params["sid"] = session_response["sid"] def list_homes(self): return self._request(action="tuya.m.location.list", version="2.1") def list_devices(self, home_id: str): return self._request( action="tuya.m.my.group.device.list", version="1.0", query_params={"gid": home_id}, )
if name == "main": eufy_client = EufyHomeSession(email=sys.argv[1], password=sys.argv[2]) user_info = eufy_client.get_user_info()
tuya_client = TuyaAPISession( username=f'eh-{user_info["id"]}', country_code=user_info["phone_code"] ) for home in tuya_client.list_homes(): print("Home:", home["groupId"]) for device in tuya_client.list_devices(home["groupId"]): print( f"Device: {device['name']}, device ID {device['devId']}, local key {device['localKey']}" )
code reviewed and can confirm functionality. Andre, this is worth a separate repository!
@cvbraeuer I’ve refactored it a bit and put it on GitLab: https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber
@Rjevski excellent, thank you for your hard work!
Cloned the repository today, and can also confirm this works out of the box. Very useful tool. Thanks
Hi guys, I'm trying to use the code @Rjevski published, but getting an issue (see traceback below), it is something related to the fact the user_id that I get has 40 characters and with 'eh-' becomes 43, that is not multiple of 16. I trucated to 16 and the error does not happen, but of course the authentication fails.
Traceback (most recent call last):
File "/usr/lib/python3.8/runpy.py", line 194, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/lib/python3.8/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/__main__.py", line 10, in <module>
for home in tuya_client.list_homes():
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 309, in list_homes
return self._request(action="tuya.m.location.list", version="2.1")
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 231, in _request
self.acquire_session()
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 305, in acquire_session
session_response = self.request_session(self.username, self.country_code)
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 279, in request_session
password = self.determine_password(username)
File "/home/lvcabral/projects/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 273, in determine_password
encrypted_uid += encryptor.finalize()
File "/home/lvcabral/.local/lib/python3.8/site-packages/cryptography/hazmat/primitives/ciphers/base.py", line 140, in finalize
data = self._ctx.finalize()
File "/home/lvcabral/.local/lib/python3.8/site-packages/cryptography/hazmat/backends/openssl/ciphers.py", line 215, in finalize
raise ValueError(
ValueError: The length of the provided data is not a multiple of the block length.
@lvcabral Does this work with the old version of the Eufy Android app someone recommended above? My reverse-engineering was based on it, I wonder if your account uses a newer auth mechanism that just wouldn't be supported by that app to begin with and would explain why my code doesn't work either.
I guess they changed something on the backend, I can login to the old app, but the device always shows as offline, in the same Android tablet, if I upgrade to the current app version, the device correctly shows as online. I managed to add the vacuum robot to the Tuya Smart app (and get the local key on their iot portal) but in the app the robot control panel is partially in Chinese and not easy to control (seems to me Eufy customized their app and left the default device panel on Tuya as the development template).
Just wondering, how did you add the vacuum to the Tuya app? If there's a way to do that (and then use the Tuya developer portal to control the device and/or get the local key) then it's probably better for everyone involved to just do that rather than reverse-engineering the Eufy app and keeping up with their updates.
It works with both Tuya Smart and Smart Life, and integrates with Home Assistant, the trade off is that the app panel, compared to Eufy one, is really bad (with Chinese options) and the Amazon Alexa integration is not enabled (probably Eufy did it via their own backend) so I would prefer to have it on Eufy app. Will keep investigating.
I have managed to use the old method (using the 2.4.0 APK) by creating a new account (and pairing the robot) using the old app, I suspect the creation of an account using newer apps is using a different type of ID and switching something on the backend that the old app (2.4.0) does not recognize. I created a new account with a different e-mail and it showed the local key. After that I managed to login on my iPhone with the latest app and it works. So I do recommend to keep the old app on an android device or VM.
@cvbraeuer I’ve refactored it a bit and put it on GitLab: https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber
So, btw, your script gave me the following error:
Traceback (most recent call last):
File "/usr/local/Cellar/python@3.9/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
return _run_code(code, main_globals, None,
File "/usr/local/Cellar/python@3.9/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code
exec(code, run_globals)
File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/__main__.py", line 10, in <module>
for home in tuya_client.list_homes():
File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 311, in list_homes
return self._request(action="tuya.m.location.list", version="2.1")
File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 260, in _request
return data["result"]
KeyError: 'result'
after a bit of troubleshooting I noticed it was this error:
{'t': 1645166510522, 'success': False, 'errorCode': 'USER_SESSION_INVALID', 'status': 'error', 'errorMsg': 'Session has expired, please log in again'}
Printing the results of all requests I noticed in the prior request, the following was part of it
{
// removed some
'mobileApiUrl': 'https://a1.tuyaus.com',
// removed some
'timezoneId': 'America/Phoenix'
// removed some
}
so a quick change in constants.py:
-TIMEZONE = "Europe/London"
+TIMEZONE = "America/Phoenix"
# from Eufy Home Android app
@@ -16,7 +16,7 @@ TUYA_CLIENT_ID = "yx5v9uc3ef9wg3v9atje"
# for other regions you may need to change this?
# TODO: is this somehow returned by the Eufy API, and if so, can we set this automatically?
-TUYA_ENDPOINT = "https://a1.tuyaeu.com/api.json"
+TUYA_ENDPOINT = "https://a1.tuyaus.com/api.json"
..and boom! it worked.
Device: Red Robot, device ID <hidden>, local key <hidden>
...so you probably should update your script to automatically set those values correctly ('registered_region'
is sent early on by Eufy Home session setup and then the values that corresponds to were in the first post of this issue).
Otherwise, just giving a heads up that I'll be attempting to re-write your code into NodeJS so I can implement it into a re-write of https://github.com/apexad/homebridge-eufy-robovac
Feel free to improve and/or fork it. Unfortunately I don't have the time to work on this further but glad to hear the community is still getting some use out of it.
On 18 Feb 2022, at 07:04, Alex Martin @.***> wrote:
@cvbraeuer I’ve refactored it a bit and put it on GitLab: https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber
So, btw, your script gave me the following error:
Traceback (most recent call last): File @.***/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 197, in _run_module_as_main
return _run_code(code, main_globals, None, File @.***/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/runpy.py", line 87, in _run_code exec(code, run_globals) File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/main.py", line 10, in
for home in tuya_client.list_homes (): File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 311, in list_homes
return self._request(action="tuya.m.location.list", version="2.1" ) File "/Users/alex.martin/git/eufy-device-id-and-local-key-grabber/eufy_local_id_grabber/clients.py", line 260, in _request
return data["result" ] KeyError: 'result' after a bit of troubleshooting I noticed it was this error:
{'t': 1645166510522, 'success': False, 'errorCode': 'USER_SESSION_INVALID', 'status': 'error', 'errorMsg': 'Session has expired, please log in again'} Printing the results of all requests I noticed in the prior request, the following was part of it
{ // removed some 'mobileApiUrl': 'https://a1.tuyaus.com', // removed some 'timezoneId': 'America/Phoenix' // removed some } so a quick change in constants.py:
-TIMEZONE = "Europe/London" +TIMEZONE = "America/Phoenix"
from Eufy Home Android app
@@ -16,7 +16,7 @@ TUYA_CLIENT_ID = "yx5v9uc3ef9wg3v9atje"
for other regions you may need to change this?
TODO: is this somehow returned by the Eufy API, and if so, can we set this automatically?
-TUYA_ENDPOINT = "https://a1.tuyaeu.com/api.json" +TUYA_ENDPOINT = "https://a1.tuyaus.com/api.json" ..and boom! it worked.
Device: Red Robot, device ID
, local key ...so you probably should update your script to automatically set those values correctly. Otherwise, just giving a heads up that I'll be attempting to re-write your code into NodeJS so I can implement it into a re-write of https://github.com/apexad/homebridge-eufy-robovac
— Reply to this email directly, view it on GitHub, or unsubscribe. Triage notifications on the go with GitHub Mobile for iOS or Android. You are receiving this because you were mentioned.
I used this tool and got deviceid and local key. Can this just be added? Sorry didn't have time to read all the issues out there on this. https://gitlab.com/Rjevski/eufy-device-id-and-local-key-grabber
This repository is down :( anyone have a fork of it?
See https://github.com/Rjevski/eufy-clean-local-key-grabber
Sent from my iPhone
On 10 Aug 2023, at 20:00, Daniel Gomez @.***> wrote:
This repository is down :( anyone have a fork of it?
— Reply to this email directly, view it on GitHubhttps://github.com/mitchellrj/eufy_robovac/issues/1#issuecomment-1673667285, or unsubscribehttps://github.com/notifications/unsubscribe-auth/AK4C6PURPRUNDTXC6EYUEXDXUUOULANCNFSM4HHQTUVA. You are receiving this because you were mentioned.Message ID: @.***>
The Robovac 30C (and presumably future models) back off onto Tuya for command and control of the devices. The Eufy integration also uses a new (or custom) URL signing mechanism for Tuya cloud APIs. All previous versions of the Tuya cloud API use variations of an MD5 (32-bit) hex digest as the request signing mechanic. The Eufy app uses some other signing mechanism with produces a 64-bit signature (possibly SHA256-based).
Authentication with Tuya itself, and fetching the device key is fairly simple (and worryingly insecure), but without understanding the signing method, we can't emulate requests.
Authenticating with Eufy
This is already well-understood and implemented in google/python-lakeside.
Fetching the Tuya settings from Eufy
The Eufy APIs serve up some information needed to link to the Tuya API.
Using those settings to log in to Tuya
Eufy takes its own user ID, formats it to an alternate ID (that it hopes doesn't overlap with an existing Tuya one), and then also encrypts that user ID to make a password. If the user doesn't exist, the Eufy app automatically registers it, and then logs in.
This is horrible security. I hate this. This isn't a bug, this is just terrible design. The only information you need to derive the user login and control their devices is the user ID (a 6-digit number).
So you now have a user ID and password. Now we just need to do the login and get the device details. This is where I'm stuck, but given a function
get_request_signature
, it works something like this...First, request a login token - this is a public key used to encrypt the login request
Then do the login/reg:
Then you list the locations (homes):
And get the devices:
Here's the utility methods used above for the Tuya part:
If anyone else can figure out the signing part, that will go a long way to resolving this.
Other approaches that haven't worked