Controlling PTZ of a Reolink camera using buttons

I got a Reolink E1 Zoom camera which I have integrated with Home Assistant using the official integration. Now I am playing around with possibilities to control the camera using buttons in a dashboard.

To start with I have created a Picture glance card according to the description the Picture glance documentation.

I have added buttons to pan the camera to the left and to the right. The problem is that when I press the buttons the camera pans correctly to the left and to the right, but with just one short press it always goes the full left (or full right), ie almost 180 degres.

So, is there any way to control the panning of the camera so it pans in smaller steps?

The yaml code for the picture glance card looks like this:

camera_view: live
type: picture-glance
entities:
  - entity: button.e1_zoom_ptz_left
    icon: mdi:pan-left
    tap_action:
      action: call-service
      service: button.press
      data:
        entity_id: button.e1_zoom_ptz_left
  - entity: button.e1_zoom_ptz_right
    icon: mdi:pan-right
    tap_action:
      action: call-service
      service: button.press
      data:
        entity_id: button.e1_zoom_ptz_right
camera_image: camera.e1_zoom_fluent
title: GƄrdsplan
1 Like

I am facing the exact same issue. Did you find an answer please?

I found the below video, seems like the ONVIF integration is better to control the PTZ for Reolink cameras (he mention this around 6:00 into the video). So I used ONVIF instead.

Thank you for your reply. I have got it working. Need to sort out 2-way audio next!

Mind sharing how you solved it?

Yeah I am pulling the video feed via webrtc and achieving ptz control via onvif. Onvif does have the video feed, but it is more laggy. I also added shortcut buttons to different ptz presets. Here’s my code if it helps…

type: custom:webrtc-camera
url: rtsp://username:password@IPaddress:554/h264Preview_01_main
style: ".mode {display: none}"
shortcuts:
  - name: Garden
    icon: mdi:flower
    service: select.select_option
    service_data:
      entity_id: select.living_room_ptz_preset
      option: Garden
  - name: Sofa
    icon: mdi:sofa
    service: select.select_option
    service_data:
      entity_id: select.living_room_ptz_preset
      option: Living Room
  - name: Sofa
    icon: mdi:dog
    service: select.select_option
    service_data:
      entity_id: select.living_room_ptz_preset
      option: Crate
  - name: Kitchen
    icon: mdi:pot-steam
    service: select.select_option
    service_data:
      entity_id: select.living_room_ptz_preset
      option: Kitchen
ptz:
  service: onvif.ptz
  data_left:
    entity_id: camera.living_room_profile000_mainstream
    pan: LEFT
    move_mode: ContinuousMove
  data_right:
    entity_id: camera.living_room_profile000_mainstream
    pan: RIGHT
    move_mode: ContinuousMove
  data_up:
    entity_id: camera.living_room_profile000_mainstream
    tilt: UP
    move_mode: ContinuousMove
  data_down:
    entity_id: camera.living_room_profile000_mainstream
    tilt: DOWN
    move_mode: ContinuousMove
  data_zoom_in:
    entity_id: camera.living_room_profile000_mainstream
    zoom: ZOOM_IN
    move_mode: ContinuousMove
  data_zoom_out:
    entity_id: camera.living_room_profile000_mainstream
    zoom: ZOOM_OUT
    move_mode: ContinuousMove
1 Like

So, your solution was to have presets to point to rather than actually solving the full-range panning issue. Correct?

No. In the code I shared there’s a ptz section, with up/down/left/right/in/out. The presets was just a nice bonus as I can achieve most the views I want with one press. I still have full manual control too.

I made an account just to say that your preset worked like a charm for my reolink camera!

Thanks @edcoppen

I get a lot of help from others. I’m pleased I managed to help out on this occasion. :blush:

1 Like

I had this same issue. I got around it by creating a script with two variables. One for the camera pan button and one for the pan stop button. I called the pan button, then a delay, then the pan stop.

sequence:
  - action: button.press
    metadata: {}
    data: {}
    target:
      entity_id:
        - "{{ camera_direction }}"
  - delay:
      hours: 0
      minutes: 0
      seconds: 0
      milliseconds: 200
  - action: button.press
    metadata: {}
    data: {}
    target:
      entity_id: "{{ camera_stop }}"
alias: Pan Camera
description: ""
fields:
  camera_direction:
    selector:
      text: null
    name: Camera Direction
    description: PTZ button to press
    required: true
  camera_stop:
    selector:
      text: null
    name: Camera Stop
    description: stop button to press
    required: true

Calling it

 - entity: button.first_floor_hall_ptz_right
    icon: mdi:pan-right
    tap_action:
      action: call-service
      perform_action: script.pan_camera
      service_data:
        camera_direction: button.first_floor_hall_ptz_right
        camera_stop: button.first_floor_hall_ptz_stop
4 Likes

Hi! how did you integrated it in a card?

Hi @bconway,

Thanks for the inspiration. :slightly_smiling_face:

I was struggling with this too.

I made an outomation, one trigger per camera triggered whenever any of the ptz buttons have been pressed for 100 ms.

Then in the actions I made a choose blok, one option for each trigger (camera) and the action is to press the stop button of that camera.

This way I adjust the behavior of the ptz buttons all over then from the dashborad I just call the button press action of the ptz buttons, these are then stopped after 100 ms or whatever time I want to set per camera.

Here is my code

alias: Reolink PTZ Control
triggers:
  - trigger: state
    entity_id:
      - button.alrum_ptz_right
      - button.alrum_ptz_left
      - button.alrum_ptz_down
      - button.alrum_ptz_up
    for:
      hours: 0
      minutes: 0
      seconds: 0.1
    id: alrum
  - trigger: state
    entity_id:
      - button.living_room_ptz_down
      - button.living_room_ptz_left
      - button.living_room_ptz_right
      - button.living_room_ptz_up
    for:
      hours: 0
      minutes: 0
      seconds: 0.1
    id: stue
conditions: []
actions:
  - choose:
      - conditions:
          - condition: trigger
            id:
              - alrum
        sequence:
          - action: button.press
            metadata: {}
            data: {}
            target:
              entity_id: button.alrum_ptz_stop
      - conditions:
          - condition: trigger
            id:
              - stue
        sequence:
          - action: button.press
            metadata: {}
            data: {}
            target:
              entity_id: button.living_room_ptz_stop
mode: single

Kind regards,
Ghassan

Thanks everyone in this thread for sharing their great suggestions.

I have tweaked @ghassan’s suggestion a bit more to easily extend to mulitple cameras - still requires adding all buttons to the automation but no need to have separate rules for separate camers:

alias: Stop camera motion
description: ""
triggers:
  - trigger: state
    entity_id:
      - button.front_door_ptz_down
      - button.front_door_ptz_right
      - button.front_door_ptz_left
      - button.front_door_ptz_up
    for:
      hours: 0
      minutes: 0
      seconds: 0.5
conditions: []
actions:
  - action: button.press
    metadata: {}
    data: {}
    target:
      entity_id: "{{ '_'.join(trigger.entity_id.split('_')[:-1]+['stop'])}}"
mode: single

Basically I cut off the name of whichever button was pressed after the last underscore and put ā€˜stop’ in that place. As long as the button names stay consistent in the integration, this should hold.

2 Likes

For those using Advanced Camera Card along with go2rc (to reduce latency) the right config is the following:

Reolink trackmix camera

- type: custom:advanced-camera-card
            cameras:
              - live_provider: go2rtc
                go2rtc:
                  url: ws://192.168.0.8:1984
                  stream: main_entrance_camera_clean_stream_1
                  prefer_webrtc: true
              - live_provider: go2rtc
                go2rtc:
                  url: ws://192.168.0.8:1984
                  stream: main_entrance_camera_clean_stream_2
                  prefer_webrtc: true
                ptz:
                  actions_left_start:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_left
                  actions_left_stop:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_stop
                  actions_right_start:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_right
                  actions_right_stop:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_stop
                  actions_up_start:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_up
                  actions_up_stop:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_stop
                  actions_down_start:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_down
                  actions_down_stop:
                    action: perform-action
                    perform_action: button.press
                    target:
                      entity_id: button.main_entrance_camera_ptz_stop

I hope this can help

Hi all,

Just wanted to share a small contribution to this excellent thread.

First of all, thanks to @bconway for the idea of calling a script directly from the picture-glance card configuration (super clean UX), and for the initial script approach. Also thanks to @Kirkebakke for the template code that derives the matching _stop entity from the direction button entity — that’s the key piece that makes a single reusable script possible.

Using both ideas, I put together a single Picture Glance card + a single script (camera_pan_tilt). The gestures are:

  • Tap → short nudge (direction press + short delay + stop)
  • Hold → longer movement (direction press + longer delay + stop)
  • Double-tap → continuous movement (single direction press, no stop)

A note about time delays: unfortunately (as far as I can tell) Reolink doesn’t expose a ā€œmove N stepsā€ or similar parameter. So we end up using delays to approximate ā€œstepsā€. This works well, but the best delay values are very dependent on the whole setup (HA hardware, VM/container, load, network latency, etc.). That also means the delay values appear many times in the card YAML, which is a bit annoying.

One possible workaround is to define delays as input_number helpers and reference them from the card. That would make tuning much easier, at the cost of adding more HA components for the same purpose (and therefore more to maintain). I haven’t implemented that version (yet), but it’s an option.

Below is what I’m currently using.

Card config (Picture Glance)

type: picture-glance
title: Entrance
camera_image: camera.entrance_fluent
camera_view: auto
fit_mode: cover

entities:
  - entity: button.entrance_ptz_left
    icon: mdi:pan-left
    tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_left
        delay_ms: 100
    hold_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_left
        delay_ms: 250
    double_tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_left
        delay_ms: 0

  - entity: button.entrance_ptz_right
    icon: mdi:pan-right
    tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_right
        delay_ms: 100
    hold_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_right
        delay_ms: 250
    double_tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_right
        delay_ms: 0

  - entity: button.entrance_ptz_up
    icon: mdi:pan-up
    tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_up
        delay_ms: 100
    hold_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_up
        delay_ms: 250
    double_tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_up
        delay_ms: 0

  - entity: button.entrance_ptz_down
    icon: mdi:pan-down
    tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_down
        delay_ms: 100
    hold_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_down
        delay_ms: 250
    double_tap_action:
      action: call-service
      service: script.camera_pan_tilt
      data:
        camera_and_direction: button.entrance_ptz_down
        delay_ms: 0

Script (scripts.yaml format)

camera_pan_tilt:
  alias: Camera Pan/Tilt (tap/hold/continuous)
  description: "Press direction button; if delay_ms>0 wait then press derived _stop."
  mode: restart
  fields:
    camera_and_direction:
      name: Camera direction button
      description: "PTZ direction button entity_id (e.g. button.entrance_ptz_down)"
      required: true
      selector:
        entity:
          domain: button
    delay_ms:
      name: Delay before stop (ms)
      description: "Milliseconds before pressing stop. Use 0 for continuous."
      required: true
      default: 100
      selector:
        number:
          min: 0
          max: 3000
          step: 50
          unit_of_measurement: ms
          mode: slider

  sequence:
    - service: button.press
      target:
        entity_id: "{{ camera_and_direction }}"

    - choose:
        - conditions: "{{ delay_ms | int > 0 }}"
          sequence:
            - delay:
                milliseconds: "{{ delay_ms | int }}"
            - service: button.press
              target:
                entity_id: >-
                  {% set domain = camera_and_direction.split('.')[0] %}
                  {% set obj = camera_and_direction.split('.')[1] %}
                  {{ domain ~ '.' ~ '_'.join(obj.split('_')[:-1] + ['stop']) }}

Best regards,
Paco