Adding PTZ control to Tuya cameras (with localtuya)

I recently got one of those cheap Tuya indoor cameras with PTZ functionality and was disappointed, that it didn’t work in Home Assistant (with official Tuya integration). This guide will show how to quickly add PTZ control from HA with the use of localtuya.

Prerequisites

  1. Working localtuya integration (will be needed as official integration doesn’t expose PTZ). Setup instructions can be found here.
  2. Working official Tuya integration (as localtuya doesn’t support camera streams)

localtuya setup

  1. open Tuya cloud dashboard, click on DevicesAll devicesDebug device (make sure to select the camera)
  2. in the opened page in the center part (Control Device with Standard section) find ptz_control and ptz_stop and take note of their values (In my case ptz_control has a value of 3 and ptz_stop is ON (so True).
  3. Click Send instruction.
  4. In localtuya add new device and look for DPs with values of ptz_control and ptz_stop. DPs should be listed in the same order as they were in the Debug device menu under Standard Instruction Set section (right side of the page). In my case I would be looking for the value of True followed by 3.
  5. Once you find those add them. Make sure the DP for ptz_control has the entity type select (with selection reflecting what was visible in Tuya dashboard, in my case 0,1,2,3,4,5,6,7)

You should now have something similar to this:

Making it more friendly to use

Right now you can control your camera by selecting a value from PTZ control entity and toggling PTZ stop, when you reach the desired position. It’s nice, but not useful. Moving directions are as follows (at least for me):
0 or 1 - up
4 or 5 - down
6 or 7 - left
2 or 3 - right

Next we’ll add a script, which we will be able to call to move the camera in a more human friendly fashion. Just copy the code from below and paste it into new script. (Home Assistant really needs a way to share scripts, just like blueprints)
YAML:

alias: Tuya PTZ
icon: mdi:camera-control
mode: single
sequence:
  - choose:
      - conditions:
          - condition: template
            value_template: "{{ direction == \"up\" }}"
        sequence:
          - service: select.select_option
            data:
              option: "0"
            target:
              entity_id: "{{ ptz_control_entity }}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: "{{ duration | default(200) }}"
          - service: switch.toggle
            data: {}
            target:
              entity_id: "{{ ptz_stop_entity }}"
      - conditions:
          - condition: template
            value_template: "{{ direction == \"down\" }}"
        sequence:
          - service: select.select_option
            data:
              option: "4"
            target:
              entity_id: "{{ ptz_control_entity }}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: "{{ duration | default(200) }}"
          - service: switch.toggle
            data: {}
            target:
              entity_id: "{{ ptz_stop_entity }}"
      - conditions:
          - condition: template
            value_template: "{{ direction == \"left\" }}"
        sequence:
          - service: select.select_option
            data:
              option: "6"
            target:
              entity_id: "{{ ptz_control_entity }}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: "{{ duration | default(200) }}"
          - service: switch.toggle
            data: {}
            target:
              entity_id: "{{ ptz_stop_entity }}"
      - conditions:
          - condition: template
            value_template: "{{ direction == \"right\" }}"
        sequence:
          - service: select.select_option
            data:
              option: "2"
            target:
              entity_id: "{{ ptz_control_entity }}"
          - delay:
              hours: 0
              minutes: 0
              seconds: 0
              milliseconds: "{{ duration | default(200) }}"
          - service: switch.toggle
            data: {}
            target:
              entity_id: "{{ ptz_stop_entity }}"

Now verify, that it’s working by calling this service (don’t forget to change the entities, duration is specified in ms):

service: script.tuya_ptz
data:
  ptz_control_entity: select.ptz_control
  ptz_stop_entity: switch.ptz_stop
  direction: up
  duration: 200

And that’s all!
Now you should be able to control the camera from HA without tuya app.

As a bonus here’s a quick card with controls included:

type: picture-glance
camera_view: auto
camera_image: camera.kamera_tuya
entities:
  - entity: select.ptz_control
    icon: mdi:arrow-up
    tap_action:
      action: call-service
      service: script.tuya_ptz
      service_data:
        ptz_control_entity: select.ptz_control
        ptz_stop_entity: switch.ptz_stop
        direction: up
  - entity: select.ptz_control
    icon: mdi:arrow-down
    tap_action:
      action: call-service
      service: script.tuya_ptz
      service_data:
        ptz_control_entity: select.ptz_control
        ptz_stop_entity: switch.ptz_stop
        direction: down
  - entity: select.ptz_control
    icon: mdi:arrow-left
    tap_action:
      action: call-service
      service: script.tuya_ptz
      service_data:
        ptz_control_entity: select.ptz_control
        ptz_stop_entity: switch.ptz_stop
        direction: left
  - entity: select.ptz_control
    icon: mdi:arrow-right
    tap_action:
      action: call-service
      service: script.tuya_ptz
      service_data:
        ptz_control_entity: select.ptz_control
        ptz_stop_entity: switch.ptz_stop
        direction: right
2 Likes

First of all, thanks for the great tutorial.

However, it wasn’t working 100% in my case, since the localtuya integration failed to read available DPs from my camera.
I had to pass the “Manual DPs” to the device, but couldn’t figure out the proper values until I have found the tinytuya python library.
This simple python script:

import tinytuya

c = tinytuya.Cloud(
    apiRegion='eu', #cn, us, us-e, eu, eu-w, or in
    apiKey='API_KEY',
    apiSecret='API_SECRET'
)

for e in c.getdps('DEVICE_ID')['result']['status']:
    print(e)

will list all the available DPs and their states + possible values.
In my case the ptz_stop was 116 and ptz_control 119.
After I have passed “116,119” as the manual DPs input, everything went further smoothly.

1 Like

Nice.

on the localtuya’s github there’s an open PR for automatic labeling of DPs: https://github.com/rospogrigio/localtuya/pull/1256. I was somewhat able to get it working with this, but it also implements a bit broken support for subnodes (here), which made it impossible for me to configure the devices.

Hello, I read your tutorial and I am extremely frustrated, because I have 4 PTZ cameras that work normally in the smart life app and in the home assistant the same cameras do not display the video stream.

I would be very grateful if someone can help me to solve this problem.
I’m using the official tuya integration with HA.

Take a look at this: Fix tuya camera stream by IceOnly · Pull Request #84094 · home-assistant/core · GitHub but keep in mind that this thread is probably not the one to report this problem.

It works like a charm! Need to pass real DPid find by tinytuya, which can’t be found at tuya cloud