i think this joins to this
You are the bomb! Have spent months trying to figure out a way to get notifications!
Thanks for the work done in node-red. Unfortunately it seems like my particular tuya doorbell is slightly different from yours and never ever presents a message with header “154”. Only a header with “115” ever shows up with information similar to what @lokster has managed to pull up. {"v":"3.0","bucket":"ty-eu-storage30-pic","files":[["/c06289-44351751-lkwl627faa9e420a6e16/detect/1707366649.jpeg","11b4aee9820c8060"]]}
If you manage to make any sense of this or could possibly assist to parse it into a URL would be great.
I worked out how to extract the image from the bucket/files message.
It was more annoying than I hoped it would be, but I got there in the end.
This link https://support.tuya.com/en/help/_detail/Kbfus79b0gcpi has some examples but the documentation it links to is missing, so i had to get the URL from squinting at the image, and it misses the decryption info.
You call a Tuya cloud API to get a URL for the file to download. That returns a file, but the contents isn’t just the actual file. It is a binary file that has a smaller header with a version and IV and then a block of AES encrypted data, for which the key is the bit after the .jpeg in the bucket/files message.
Here’s a python script that takes the base64 encoded raw DPS data and downloads and decrypts the file
from tuya_connector import TuyaOpenAPI
import base64
import sys
import json
from urllib.request import urlopen
from Crypto.Cipher import AES
import struct
import io
BLOCK_SIZE = 16
def pad(byte_array:bytearray):
pad_len = BLOCK_SIZE - len(byte_array) % BLOCK_SIZE
return byte_array + (bytes([pad_len]) * pad_len)
def unpad(s:bytearray):
return s[:-ord(s[len(s)-1:])]
ACCESS_ID = "********************"
ACCESS_KEY = "********************************"
API_ENDPOINT = "https://openapi.tuyaeu.com" # change for your own use case
DEVICE_ID = "**********************"
# Init OpenAPI and connect
openapi = TuyaOpenAPI(API_ENDPOINT, ACCESS_ID, ACCESS_KEY)
openapi.connect()
base64String = sys.argv[1]
decoded = json.loads(base64.b64decode(base64String))
bucket = decoded["bucket"]
file = decoded["files"][0][0]
key = decoded["files"][0][1].encode('utf-8')
fileURLFetch = openapi.get("/v1.0/devices/{0}/movement-configs?bucket={1}&file_path={2}".format(DEVICE_ID, bucket, file))
actualFileURL = fileURLFetch["result"]
fileContents = urlopen(actualFileURL).read()
with io.BytesIO(fileContents) as src_file:
# seems to be 1, which
version = struct.unpack('i', src_file.read(4))[0]
iv = src_file.read(16)
src_file.read(44)
file_contents = src_file.read()
cipher = AES.new(key, AES.MODE_CBC, iv)
result = cipher.decrypt(pad(file_contents))
with open("my_file.jpg", "wb") as binary_file:
binary_file.write(result)
From my investigation, based version it looks like there could be different cyphers used, but this seems to work for my camera at least.
I hope it helps!
Thank you, Paul. That seems like what I needed. However, it seems like this API is no longer working, right? I can’t find it in the API Explorer or the documentation.
Edit: I needed to authorize the Beta API
Is there any new working solution, maybe?
I used above python code but cannot get any success still getting the error the api sit not subscribed,
also i have been trying to use pulsar and i can create full url (https://ty-eu-storage30.s3.eu-central-1.amazonaws.com/302ac0-19635407-pp0112a26b214cdf372b/detect/161720xxxx.jpeg) but dunno what to do next.
Hello,
I’ve been using this NodeRed tips for a few months with my Tuya Doorbell, but since 2024-10-23, I no longer receive messages with dps 154 (nor many others). I may have made something wrong, but I’d like to know if it continues to work for you.
Many thanks for your help
Насчет node red не скажу, у меня через tuyaapi не видно doorbell, но у меня работает скрипт на питоне по аналогии Make the picture taken from Tuya Smart Video Doorbell available in HA - #24 by Belzedaar. Интересно?
is this actually?
I contacted my device’s support team, but haven’t heard back yet. However, everything seems to be working again now. I can see the DPS154 (doorbell_pic) clearly.
Да, интересно , если не сложно поделитесь решением
For all those who still receives pictures in DPS 185, I have created a node-red-contrib-tuya-file-decrypt package to ease the task.
It is paramount to enable the Beta API on your Tuya IOT Platform (Cloud → [Your Project] → Service API and activate “Beta API”)
I have updated the @zelo66 example flow with it. Import and adapt to your device:
[{"id":"a34c6d7687862b38","type":"tuya-smart-device","z":"39ee9e117a73dccf","deviceName":"YOUR_DEVICE_NAME","disableAutoStart":false,"deviceId":"YOUR_DEVICE_ID","deviceKey":"YOUR_DEVICE_KEY","storeAsCreds":false,"deviceIp":"YUOR_DEVICE_IP","retryTimeout":1000,"findTimeout":10000,"tuyaVersion":"3.3","eventMode":"event-both","logLevel":"log-level-disable","x":150,"y":120,"wires":[["7312bc5118184296"],[]]},{"id":"ef68fc4a1fdb23b4","type":"function","z":"39ee9e117a73dccf","name":"Add timestamp to LOG","func":"var now = new Date();\n// Create formatted time\nvar yyyy = now.getFullYear();\nvar mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nvar dd = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nvar hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nvar mmm = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nvar ss = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\nvar time = yyyy + \"-\" + mm + \"-\" + dd + \" \" + hh + \":\" + mmm + \":\" + ss;\n\nmsg.payload = { Time: time, Message: msg.payload }\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":420,"y":120,"wires":[["039addf423dbf3e6"]]},{"id":"039addf423dbf3e6","type":"file","z":"39ee9e117a73dccf","name":"Save all commands send by dorbell in /homeassistant/www/YOUR_DEVICE_NAME/YOUR_DEVICE_NAME.log","filename":"/homeassistant/www/YOUR_DEVICE_NAME/YOUR_DEVICE_NAME.log","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":990,"y":120,"wires":[[]]},{"id":"ed338d654a93dba7","type":"inject","z":"39ee9e117a73dccf","name":"Fake message","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"data\":{\"dps\":{\"185\":\"aHR0cHM6Ly9yZWdpb3dlYmNhbS5kZS9maWxlYWRtaW4vdXNlcl91cGxvYWQvMDFfc3RvcmNoX2tpcmNoemFydGVuMjAyMS5qcGc/dmVyPTE2MzkzMDIyNjYzNDk=\"},\"t\":1664969089},\"deviceId\":\"bfd52cb7f15a171ef3qznk\",\"deviceName\":\"YOUR_DEVICE_NAME\"}","payloadType":"json","x":160,"y":200,"wires":[["7312bc5118184296"]]},{"id":"7312bc5118184296","type":"switch","z":"39ee9e117a73dccf","name":"If contains picture","property":"payload.data.dps","propertyType":"msg","rules":[{"t":"hask","v":"185","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":410,"y":200,"wires":[["f1a4eb93520a0d83","ef68fc4a1fdb23b4"],[]]},{"id":"f1a4eb93520a0d83","type":"change","z":"39ee9e117a73dccf","name":"Extract base64 adress","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.data.dps[\"185\"]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":680,"y":200,"wires":[["4945bd2e04b6bc22"]]},{"id":"4945bd2e04b6bc22","type":"tuya-file-decrypt","z":"39ee9e117a73dccf","name":"","accessId":"YOUR_ACCESS_ID","accessKey":"YOUR_ACCESS_KEY","endpoint":"https://openapi.tuyaeu.com","deviceId":"YOUR_DEVICE_ID","logLevel":"log-level-disable","x":950,"y":200,"wires":[["a519f48843978de1"]]},{"id":"a519f48843978de1","type":"change","z":"39ee9e117a73dccf","name":"Set msg.image","rules":[{"t":"set","p":"payload","pt":"msg","to":"image","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":200,"y":280,"wires":[["f641d45cd10e7f3f"]]},{"id":"f641d45cd10e7f3f","type":"function","z":"39ee9e117a73dccf","name":"Set file name","func":"var now = new Date();\n// Create formatted time\nvar yyyy = now.getFullYear();\nvar mm = now.getMonth() < 9 ? \"0\" + (now.getMonth() + 1) : (now.getMonth() + 1); // getMonth() is zero-based\nvar dd = now.getDate() < 10 ? \"0\" + now.getDate() : now.getDate();\nvar hh = now.getHours() < 10 ? \"0\" + now.getHours() : now.getHours();\nvar mmm = now.getMinutes() < 10 ? \"0\" + now.getMinutes() : now.getMinutes();\nvar ss = now.getSeconds() < 10 ? \"0\" + now.getSeconds() : now.getSeconds();\n\n// file path with / at the end\nvar path = \"/homeassistant/www/YOUR_DEVICE_NAME/\"; // This is the path\nvar fileName = \"YOUR_DEVICE_NAME_\" + yyyy + \"_\" + mm + \"_\" + dd + \"-\" + hh + \"_\" + mmm + \"_\" + ss + \".jpg\"; // file name\n//var pathWithLocal = \"/homeassistant/www/YOUR_DEVICE/\" // Need to add /local/ to path for notify\n//node.warn(now);\n//msg.filename = `/share/logs/test.jpg`\n\nmsg.filename = path + fileName;\n//msg.filename2 = pathWithLocal + fileName;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":280,"wires":[["925ea5576fa5f079"]]},{"id":"925ea5576fa5f079","type":"file","z":"39ee9e117a73dccf","name":"Write file","filename":"filename","filenameType":"msg","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"none","x":600,"y":280,"wires":[[]]},{"id":"518091e2c96c8881","type":"global-config","env":[],"modules":{"node-red-contrib-tuya-smart-device":"5.4.0","node-red-contrib-tuya-file-decrypt":"1.0.3"}}]
I have drafted on Github a second package called node-red-contrib-tuya-camera-snapshot in order to invoke the /v1.0/cameras/${deviceId}/actions/capture Tuya API, but as far as my device does not support this API, I could neither test it nor publish it.