Make the picture taken from Tuya Smart Video Doorbell available in HA

On my case, I am not receiving any state for doorbell_pic, it is not exposed through MQTT, marked as binary but I can alarm_message attribute and it is in base64. If I decode it, I can see the event type ipc_doorbell or ipc_movement and s3 bucket address. but, I could not reach out to exact file with s3 bucket information.

Hi, i’m not using home assistant but am developing my own domotics management tool. I noticed this question on how to get the doorbell_pic from tuya service bus message.

I got this working through some article i found on the iot tuya developer pages, which describes the ‘encryption’ of the messages. Just check the demo pulsar project (message queue consumer), for DecryptByAes Get Push Messages by Pulsar (C# SDK)-Tuya IoT Development Platform-Tuya Developer

I managed to get a (working) image url of my doorbell (attention, it is only valid for a limited period of time).

How to store in home assistant or things, i don’t know, i store it locally in my own sql db

I hope this helps someone.

And maybe someone can help me: i have the message queue listener up and running, but when someone rings my doorbell, i only get the ‘doorbell_pic’ notification once in a while, any idea ? However, in my smart life app, i can see all pictures of all people ringing my doorbell

I tried this (the python version of the example - Get Push Messages by Pulsar (Python SDK)-Tuya IoT Development Platform-Tuya Developer), but the data I can get with it is more or less the same I get from the API - it contains the same movement_detect_pic and alarm_message data I was already able to get. Something like this after base64 decoding:

{"v":"3.0","bucket":"ty-eu-storage30","files":[["/555455-18709973-ylsda70cecbcc17af30b/detect/1638478699.jpeg","1a54e0f1b0f94582"]]}

The contents of the message when someone rings the doorbell are a bit different, but the problem is the same - the value of the “alarm_message” can be base64 decoded to something like this:

{"v":"3.0","files":[{"data":"1c660cc9e0cc914b57dfcbfc228374029edb76815475801099cf33f6aa1bd150a1153e61f756bda2e18949ee47ae2e73a93bfcb21b266fccdd1840a87ee7535c49dfff4440d0a21b92d040538be97d709a9f68759bbfa489c07bef8722cad996aa1a89a6aee0d78a7bf64351633136f1","keyId":"default","iv":"c6974bc7d7079637257222c28e425a21"}],"cmd":"ipc_doorbell","type":"image"}

How do you get the correct url of the picture?
Can you provide an example how you do it?

Hi, Just receive a battery tuya doorbell for christmas. I read a lot about it and how much is a Headaches.

I have a some question :

1: Instead of asking for the feature of the picture, why just ask for a better power management ? like awake the camera only if I check the feed… ( bonus tell me when the camera is awake so i can take a screenshot of the movement )

2: Do you guys check on the side of LocalTuya ( custom integration ) it does’nt seam to be working but you are searching for a longer time than me may be you saw something i did’nt see

3: At this point I don’t mind to keep the doorbell plug into wall outlet and just use it as wireless camera but my issue is the camera feed is too random … almost always not working i’m i the only one ?

thanks

EDIT : i think I found what you need: Every time the doorbell detect motion it become idle then unavailable … so if you do an automation that look like that you should receive a notification with the image :

alias: TestDoorbell
description: ""
trigger:
  - platform: state
    entity_id:
      - camera.porrte_avant
    to: idle
condition: []
action:
  - service: camera.snapshot
    data:
      filename: /config/www/snapshots/test.jpg
    target:
      entity_id: camera.porrte_avant
  - service: notify.mobile_app_djjfj
    data:
      message: Test DoorBell
      data:
        image: https://YOURIP/local/snapshots/test.jpg
mode: single

Unfortunately this image is not the image that the camera took when it detected the motion (and not the one from the notification in SmartLife / Tuya app). Hence - it’s delayed, because it takes some time for the camera to initially wake on movement, then it takes some time for the tuya integration to realize that the camera is awake, and then it takes even more time to take the snapshot.
And I think it’s critical to get the very first snapshot - as soon as the camera wakes up, otherwise it might and will miss some important “action” in front of the door.
Btw, regarding detecting when the camera is awake (i.e. when there is motion) - I’m using a faster method with a ping command, emulating motion sensor. It’s fast - no more than 1 second delay after the camera awakes:

binary_sensor:
  - platform: command_line
    name: "Motion - Door"
    command: 'ping -W 1 -c 1 THE_CAMERA_IP > /dev/null 2>&1 && echo on || echo off'
    device_class: motion
    payload_on: "on"
    payload_off: "off"
    scan_interval: 1
1 Like

Hey,

I’ve been wondering for a long time how to download the image taken by my crappy video intercom (it’s one of the worst things I bought that was supposed to be smart). I found an option to capture a message containing a picture. It’s the same photo that’s in the official Tuya app. I used Node-RED installed in HA for this, because you can simulate messages and debug the whole process accurately.

In Node-RED I used this plugin ( node-red-contrib-tuya-smart-device (node) - Node-RED ) In Node-RED you can install it by clicking on Settings → Palette → Install → and search for “node-red-contrib-tuya-smart-device”

Import the code that is below:

[{"id":"0f190a2bca60a4be","type":"tuya-smart-device","z":"8ad639860cb6690f","deviceName":"Dorbell parter","disableAutoStart":false,"deviceId":"","deviceKey":"","storeAsCreds":false,"deviceIp":"10.0.1.50","retryTimeout":1000,"findTimeout":10000,"tuyaVersion":"3.3","eventMode":"event-both","credentials":{"secretConfig":"{}"},"x":340,"y":480,"wires":[["5afadc8ea527fdf3","8938329798814a30"],[]]},{"id":"5afadc8ea527fdf3","type":"function","z":"8ad639860cb6690f","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":480,"wires":[["a25ea003dfe40c1b"]]},{"id":"a25ea003dfe40c1b","type":"file","z":"8ad639860cb6690f","name":"Save all commands send by dorbell in /share/logs/parter.log","filename":"/share/logs/parter.log","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":960,"y":480,"wires":[[]]},{"id":"1286683668fb701f","type":"base64","z":"8ad639860cb6690f","name":"Decode base64 adress","action":"b64","property":"payload","x":1030,"y":560,"wires":[["0b9c9d503cf1a34a"]]},{"id":"710cf8f19245a997","type":"inject","z":"8ad639860cb6690f","name":"Fake message","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"data\":{\"dps\":{\"154\":\"aHR0cHM6Ly9yZWdpb3dlYmNhbS5kZS9maWxlYWRtaW4vdXNlcl91cGxvYWQvMDFfc3RvcmNoX2tpcmNoemFydGVuMjAyMS5qcGc/dmVyPTE2MzkzMDIyNjYzNDk=\"},\"t\":1664969089},\"deviceId\":\"bfd52cb7f15a171ef3qznk\",\"deviceName\":\"Dorbell pietro\"}","payloadType":"json","x":360,"y":560,"wires":[["8938329798814a30"]]},{"id":"8938329798814a30","type":"switch","z":"8ad639860cb6690f","name":"If contains picture","property":"payload.data.dps","propertyType":"msg","rules":[{"t":"hask","v":"154","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":570,"y":560,"wires":[["49ff43949c4ef580"],[]]},{"id":"49ff43949c4ef580","type":"change","z":"8ad639860cb6690f","name":"Extract base64 adress","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.data.dps[\"154\"]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":560,"wires":[["1286683668fb701f"]]},{"id":"45dc7a80ee0db931","type":"file","z":"8ad639860cb6690f","name":"Write file","filename":"filename","filenameType":"msg","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":840,"y":640,"wires":[["74cae47ba499b0ac"]]},{"id":"b5a8447904c662d5","type":"function","z":"8ad639860cb6690f","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 = \"/media/Parter/\";                     // This is the path\nvar fileName = \"Dorbell_Parter_\" + yyyy + \"_\" + mm + \"_\" + dd + \"-\" + hh + \"_\" + mmm + \"_\" + ss + \".jpg\";     // file name\nvar pathWithLocal = \"/media/local/Parter/\" // Need to add /local/ to path for notify\n//node.warn(now);\n//msg.filename = `/share/logs/test.jpg`\n\nmsg.filename = path + fileName;\nmsg.filename2 = pathWithLocal + fileName;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":640,"wires":[["45dc7a80ee0db931"]]},{"id":"8049a89b539eb455","type":"http request","z":"8ad639860cb6690f","name":"HTTP request","method":"use","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":520,"y":640,"wires":[["b5a8447904c662d5"]]},{"id":"0b9c9d503cf1a34a","type":"change","z":"8ad639860cb6690f","name":"Payload to URL","rules":[{"t":"set","p":"url","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"method","pt":"msg","to":"get","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":640,"wires":[["8049a89b539eb455"]]},{"id":"7642b2b6835a4e61","type":"tuya-smart-device","z":"8ad639860cb6690f","deviceName":"Dorbell pietro","disableAutoStart":false,"deviceId":"","deviceKey":"","storeAsCreds":false,"deviceIp":"10.0.1.51","retryTimeout":1000,"findTimeout":10000,"tuyaVersion":"3.3","eventMode":"event-both","credentials":{"secretConfig":"{}"},"x":350,"y":820,"wires":[["7b321904fe0af295","27138d1448befa47"],[]]},{"id":"cc076642764c123e","type":"file","z":"8ad639860cb6690f","name":"Save all commands send by dorbell in /share/logs/pietro.log","filename":"/share/logs/pietro.log","filenameType":"str","appendNewline":true,"createDir":true,"overwriteFile":"false","encoding":"none","x":960,"y":820,"wires":[[]]},{"id":"7b321904fe0af295","type":"function","z":"8ad639860cb6690f","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,"noerr":0,"initialize":"","finalize":"","libs":[],"x":600,"y":820,"wires":[["cc076642764c123e"]]},{"id":"43a86da92e358c4d","type":"base64","z":"8ad639860cb6690f","name":"Decode base64 adress","action":"b64","property":"payload","x":1030,"y":900,"wires":[["ce17f74d19a20be1"]]},{"id":"931fd56e0ecdcad9","type":"inject","z":"8ad639860cb6690f","name":"Fake message","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"data\":{\"dps\":{\"154\":\"aHR0cHM6Ly9yZWdpb3dlYmNhbS5kZS9maWxlYWRtaW4vdXNlcl91cGxvYWQvMDFfc3RvcmNoX2tpcmNoemFydGVuMjAyMS5qcGc/dmVyPTE2MzkzMDIyNjYzNDk=\"},\"t\":1664969089},\"deviceId\":\"bfd52cb7f15a171ef3qznk\",\"deviceName\":\"Dorbell pietro\"}","payloadType":"json","x":360,"y":900,"wires":[["27138d1448befa47"]]},{"id":"27138d1448befa47","type":"switch","z":"8ad639860cb6690f","name":"If contains picture","property":"payload.data.dps","propertyType":"msg","rules":[{"t":"hask","v":"154","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":570,"y":900,"wires":[["1e7bc8ea60e2b55c"],[]]},{"id":"1e7bc8ea60e2b55c","type":"change","z":"8ad639860cb6690f","name":"Extract base64 adress","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.data.dps[\"154\"]","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":900,"wires":[["43a86da92e358c4d"]]},{"id":"e372f3b41528dc38","type":"file","z":"8ad639860cb6690f","name":"Write file","filename":"filename","filenameType":"msg","appendNewline":true,"createDir":false,"overwriteFile":"true","encoding":"none","x":840,"y":980,"wires":[["2262904dd9aedfef"]]},{"id":"c8ca7efa2c8f033c","type":"function","z":"8ad639860cb6690f","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 = \"/media/Pietro/\";                     // This is the path\nvar fileName = \"Dorbell_Pietro_\" + yyyy + \"_\" + mm + \"_\" + dd + \"-\" + hh + \"_\" + mmm + \"_\" + ss + \".jpg\";     // file name\nvar pathWithLocal = \"/media/local/Pietro/\" // Need to add /local/ to path for notify\n\nmsg.filename = path + fileName;\nmsg.filename2 = pathWithLocal + fileName;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":690,"y":980,"wires":[["e372f3b41528dc38"]]},{"id":"ca1df8d17e7e2c27","type":"http request","z":"8ad639860cb6690f","name":"HTTP request","method":"use","ret":"bin","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":520,"y":980,"wires":[["c8ca7efa2c8f033c"]]},{"id":"ce17f74d19a20be1","type":"change","z":"8ad639860cb6690f","name":"Payload to URL","rules":[{"t":"set","p":"url","pt":"msg","to":"payload","tot":"msg"},{"t":"set","p":"method","pt":"msg","to":"get","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":980,"wires":[["ca1df8d17e7e2c27"]]},{"id":"252fe9520b79ef56","type":"api-call-service","z":"8ad639860cb6690f","name":"Notify","server":"32589209.0c3ffe","version":5,"debugenabled":true,"domain":"notify","service":"mobile_app_telefonadrian","areaId":[],"deviceId":[],"entityId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1290,"y":640,"wires":[[]]},{"id":"74cae47ba499b0ac","type":"delay","z":"8ad639860cb6690f","name":"Delay 1s","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":980,"y":640,"wires":[["05fa85fd24f29686"]]},{"id":"05fa85fd24f29686","type":"function","z":"8ad639860cb6690f","name":"Build message","func":"msg.payload =\n{\n    \"data\":\n    {\n        \"title\": \"Ktoś dzwoni!\",\n        \"message\": \"Ktoś dobija się do domu! (zjęcie z parteru)\",\n        \"data\":\n        {\n            \"ttl\": 0,\n            \"priority\": \"high\",\n            \"icon_url\": msg.filename2,\n            \"color\": \"green\",\n            \"image\": msg.filename2,\n            \"sticky\": \"true\",\n        }\n    }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1140,"y":640,"wires":[["252fe9520b79ef56","f4c7478e2a586590"]]},{"id":"2be6e90b7f5673df","type":"api-call-service","z":"8ad639860cb6690f","name":"Notify","server":"32589209.0c3ffe","version":5,"debugenabled":true,"domain":"notify","service":"mobile_app_telefonadrian","areaId":[],"deviceId":[],"entityId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1290,"y":980,"wires":[[]]},{"id":"2262904dd9aedfef","type":"delay","z":"8ad639860cb6690f","name":"Delay 1s","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":980,"y":980,"wires":[["56aed77283525840"]]},{"id":"56aed77283525840","type":"function","z":"8ad639860cb6690f","name":"Build message","func":"msg.payload =\n{\n    \"data\":\n    {\n        \"title\": \"Ktoś dzwoni!\",\n        \"message\": \"Ktoś dobija się do domu! (zjęcie z piętra)\",\n        \"data\":\n        {\n            \"ttl\": 0,\n            \"priority\": \"high\",\n            \"icon_url\": msg.filename2,\n            \"color\": \"green\",\n            \"image\": msg.filename2,\n            \"sticky\": \"true\",\n        }\n    }\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1140,"y":980,"wires":[["2be6e90b7f5673df","23a73e15a77f75ab"]]},{"id":"f4c7478e2a586590","type":"api-call-service","z":"8ad639860cb6690f","name":"Notify","server":"32589209.0c3ffe","version":5,"debugenabled":true,"domain":"notify","service":"mobile_app_sm_a505fn","areaId":[],"deviceId":[],"entityId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1290,"y":700,"wires":[[]]},{"id":"23a73e15a77f75ab","type":"api-call-service","z":"8ad639860cb6690f","name":"Notify","server":"32589209.0c3ffe","version":5,"debugenabled":true,"domain":"notify","service":"mobile_app_sm_a505fn","areaId":[],"deviceId":[],"entityId":[],"data":"","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1290,"y":1040,"wires":[[]]},{"id":"32589209.0c3ffe","type":"server","name":"Home Assistant","version":4,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":"at: ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"h23","statusTimeFormat":"h:m"}]

It contains the process of saving all the messages sent by each device (I have two monitors so I created two copies). In addition, the pictures are saved in a folder. And at the very end a notification is sent to the phone.

In order for everything to work you need to enter “Device Virtual ID”, " Device Key" and “IP adress”.

You can easily check the Device Virtual ID in TuyaIOT.

Device Key is also in TuyaIOT but is a little harder to get. You need to go to “Api Explorer” on the TuyaIOT website and search for “Get Device Information”. It should be under “General Device Capabilities” → “General device management” → “Get Device Information”.

Now you need to paste the “Device ID” there and press Submit Request. If your device responds you should get the device information, and in the ““local_key”:” line is your Device Key.

I spent a few hours on it but for about two months now everything has been working properly.

6 Likes

Thanks for sharing, with this approach, you are extracting the message or photo?

A message containing an encrypted link to the image. The image is uploaded by dorbell to the tuya server and is available for download for one minute. I uploaded the whole process in the picture in the previous post.

1 Like

Can you share the node details of decode, payload, url generation etc? I can quickly come up with a custom tuya camera integration to take care of this inside home assistant

1 Like
{"data":{"dps":{"110":1},"t":xxxxxx},"deviceId":"xxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"109":"15586432|1347888|14238544"},"t":xxxxxxx},"deviceId":"xxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"117":0},"t":xxxxxxx},"deviceId":"xxxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"231":"{\"cc\":1,\"chs\":[{\"id\":1,\"n\":\"channel 1\"},{\"id\":2,\"n\":\"channel 2\"},{\"id\":3,\"n\":\"channel 3\"},{\"id\":4,\"n\":\"channel 4\"}]}"},"t":xxxxxx},"deviceId":"xxxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"154":"aHR0cHM6Ly90eS1ldS1zdG9yYWdlMzAtcGljLnMzLmV1LWNlbnRyYWwtMS5hbWF6b25hd3MuY29tL2ZmMzU2Yi0zNDczNTc3NS1ia2R6OWM5OTNhN2QzYWVkMjI3Yy9kZXRlY3QvMTY3MjI0MjA0OS5qcGVnP1gtQW16LVNlY3VyaXR5LVRva2VuPUZ3b0daWEl2WVhkekVCRWFERzIydm1oUU1pMTBEcE9QUmlLRkFnbkxzYzJXMHlBNlBGUm9xUjg4UEFjWk5xVXgxU1I4aVcwNGhKJTJCT05PZ0ZKN3pDRUl2WXhHaHFQQUxiNU1sJTJGQ1lQTWR1cWIyUVFMVk4xRnNQMnQ2a3BxeXlWZ3NUbmhPc2I2OWh0UzltYlo4WDQ2QVVGaVNyMFlNSU9ucjduOFFxZnJVekZtbzcxNzlyd1RpUVZ2bVhzMXJNT090ckxqVE5mb3M3d2MxNzlTM3lRa3VaVjRacUNpZ0IlMkJ1V3gxeTVCZmVoSk1rV3hPcHJNUG43dldNUTJoV2VzdkVHMG1GZDIyM0ZwRU1kcnRQdjc3UVZZc2tCczQlMkZDRGk1UzhNYXN5VyUyRlFqaGFtOVNVSFl1aWJVczQ2R0lvaG9iRTI1SUxsaUFMYWlnS1dIU3pyeDVJNWZiWmMlMkZzbkVTQXhCc3NPSXJCZ1BPRmNGUTJadU52SXFXNk1mVHA2Umw5MXFTaUN4N0dkQmpJcGpTY0d0bU5XRHRYWG1nSWdtSmEyd3YwcnpETUFEczElMkJoNzdReXRIbno5dGhWMSUyQnQwdXNGdE04JTNEJlgtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QVNJQVVUUE1VSkpKNUdBWkRFUDYlMkYyMDIyMTIyOCUyRmV1LWNlbnRyYWwtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyMjEyMjhUMTU0MDUyWiZYLUFtei1FeHBpcmVzPTYwJlgtQW16LVNpZ25lZEhlYWRlcnM9aG9zdCZidWNrZXQ9dHktZXUtc3RvcmFnZTMwLXBpYyZ2PTEuMCZYLUFtei1TaWduYXR1cmU9MjY4Y2M5N2YyMTFjMmNlYmE0NDFlYTBlMmQxNWExYTQ0ZmFmZmQzYjZiZjY1YmExMjlhMjgzNTM4MTA3YTk5OQ=="},"t":xxxxx},"deviceId":"xxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"154":"aHR0cHM6Ly90eS1ldS1zdG9yYWdlMzAtcGljLnMzLmV1LWNlbnRyYWwtMS5hbWF6b25hd3MuY29tL2ZmMzU2Yi0zNDczNTc3NS1ia2R6OWM5OTNhN2QzYWVkMjI3Yy9kZXRlY3QvMTY3MjMyOTM1My5qcGVnP1gtQW16LVNlY3VyaXR5LVRva2VuPUZ3b0daWEl2WVhkekVDa2FETzBLT3dmMUZOSGxKS2Izd0NLRkFxYUR6RWZlWUlnNFVoZUxFTnFiRGo3R1hIYXVrcGNpUzI3dXRuSmJuQ3kwTFd2QnFORXRsc2lqMGF1MktXVHIydEw0R0RtZ1lRMU5jN1pnVFdCWU9VNjdGS01wSVBkSjdzcWhkTFdjVjIxZmxpeGxZTnQ3cXlRaDk4Y2Vlejl3c0dBQyUyQldycjcyaDcxQ3AlMkZwUkJtZkxTaWp3enVma1diRlpvSEtPdlMyRGM1QzFvcTdBUllyRTU5dW1kdDNSMU5qTU92Wmd6WWRNdTJpdE85QW5oOE5EMWklMkIwNm4lMkYlMkY3ZjVlejExQzl4MzV0eTk2UUxKTGgwZkZuaGtiS1pmQUp2UTVLMzQ4ZjBNUnc3OUY2alJPb0hVYTVUWCUyRkh0c1diZ3RqdkxWV09Ra0NRUURPeEVadzBNMzlsZjBpS0tCMkN3SHpNbWJRVnVRJTJCSktDWFNKOCUyQkxueUtrRnpZYzJ3U2lJOGJhZEJqSXBUTFBZYTBDOExLQ2pNTzBRekZocXhvRk12a0RpayUyQlRuc0ptb29KcmdvVnhpVGE2YlB6VGVFUDAlM0QmWC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BU0lBVVRQTVVKSkpSU1RGUlBWQSUyRjIwMjIxMjI5JTJGZXUtY2VudHJhbC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIyMTIyOVQxNTU1NTZaJlgtQW16LUV4cGlyZXM9NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmJ1Y2tldD10eS1ldS1zdG9yYWdlMzAtcGljJnY9MS4wJlgtQW16LVNpZ25hdHVyZT1kMGYyMWQ4ZjY1NDcxODczYzgxNGJlYTVlMTZlNmIzNDU3MWY1MWNiMjIxMzg2YjM2MjEzOTE3ZmE3Nzc5YzIx"},"t":xxxxxx},"deviceId":"xxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"154":"aHR0cHM6Ly90eS1ldS1zdG9yYWdlMzAtcGljLnMzLmV1LWNlbnRyYWwtMS5hbWF6b25hd3MuY29tL2ZmMzU2Yi0zNDczNTc3NS1ia2R6OWM5OTNhN2QzYWVkMjI3Yy9kZXRlY3QvMTY3MjQ3OTQxNi5qcGVnP1gtQW16LVNlY3VyaXR5LVRva2VuPUZ3b0daWEl2WVhkekVGTWFETkc3UjJFbDE2cFlOWFloWlNLRkFpRHF2anNWRXN3dTBnckZIWXVZODZYY1l4NWdLJTJGTU9MdlE1Q2ZyYTY3bUQlMkY5dEhwcGx5WWlZOXYyMHU2SGRZbFRVRnEyUFhic0dNVkt3SDJDMjZNSDh1b1pZTVNOaVhZZXVYOXd0bUpPeDZIbVZ2bTl3NnplQTBhWUVUc0tzek9ESkJINkhwdCUyRkxCcGdaNHZKS2xzelp2S1hVSlBhczZycXV0OTBpWmUwNWRhN1VweVolMkZJSHRTWmhKRlRSRUMwZ2JGS1VGbDFnbGlnRW1aT1pPM2pWbURuZWpzMnZBNmNqTkI2RHppNCUyQlVKTmlRSHQ2OXNWYlZxWHhoOWtwdzN2ZEN2RXhZRmUxdmolMkZ1SFBXODA3cEJaWTFuZHpuRFN2eiUyRnRxMXpJc05JalZnUUtldEczMkRFVERjdmRrNkxiOUtiVmszdVFYTlFRUzBrMVFjWVNNc0dIaVhVTWY2NnlpMmhjQ2RCaklwJTJGQnllaEhzWE14VnluaGRpUm0xV1J6WXdGTUxDTzUlMkZKMVpuWmdqeVJFa2VYaDNDbnY1TFUwcmslM0QmWC1BbXotQWxnb3JpdGhtPUFXUzQtSE1BQy1TSEEyNTYmWC1BbXotQ3JlZGVudGlhbD1BU0lBVVRQTVVKSkpTNEpXNEc0UiUyRjIwMjIxMjMxJTJGZXUtY2VudHJhbC0xJTJGczMlMkZhd3M0X3JlcXVlc3QmWC1BbXotRGF0ZT0yMDIyMTIzMVQwOTM2NThaJlgtQW16LUV4cGlyZXM9NjAmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmJ1Y2tldD10eS1ldS1zdG9yYWdlMzAtcGljJnY9MS4wJlgtQW16LVNpZ25hdHVyZT1iNjk3YWFhODE4MDExYzhmNGIyODU4NmY4ZDhiODFhMGNjNjNhMjI0MWFlYWJlODNmYWYwNjNlYzkzYjdlYzRj"},"t":xxxxxx},"deviceId":"xxxxxxxx","deviceName":"Dorbell parter"}}
{"data":{"dps":{"154":"aHR0cHM6Ly9yZWdpb3dlYmNhbS5kZS9maWxlYWRtaW4vdXNlcl91cGxvYWQvMDFfc3RvcmNoX2tpcmNoemFydGVuMjAyMS5qcGc/dmVyPTE2MzkzMDIyNjYzNDk="},"t":xxxxxx},"deviceId":"xxxxxx","deviceName":"Dorbell pietro"}

This is how the data sent automatically by doorbell looks like. When the doorbell button is activated, a command with the header “154” is sent. I check if the message has this header.

If it does, the function decodes the address and we get a normal Internet address containing the image.

I don’t have anything to show because all these functions are built into Node-RED. Download it and check it yourself if you want.

4 Likes

Thanks, will do this weekend and share my findings

2 Likes

Any luck? :slight_smile:

2 Likes

i’m. interested too

1 Like

Is the problem solved?
i want to buy a Tuya doorbell

Is there any update? I also have a Tuya doorbell powered with battery.

i think this joins to this

You are the bomb! Have spent months trying to figure out a way to get notifications!

1 Like

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!

1 Like

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