Lutron Caseta Pico Remove - Double/Long Press - New Approach

Pico Click: Double & Long Press for All Lutron Pico Remotes (Custom Integration)

I built a custom integration that adds single, double, and long press detection to all Lutron Caseta Pico remotes. No per-remote configuration, no blueprints, no replacing the core integration.

The Problem

The core Lutron Caseta integration only gives you raw press and release events for Pico remotes. If you want double-press or long-press actions, the existing solutions require either:

  • A blueprint per remote (with separate automations for each Pico)
  • A fork that replaces the core Caseta integration files
  • Double-press on raise/lower buttons is often disabled as “bad experience”

The Solution

pico_click is a standalone custom component that listens for raw lutron_caseta_button_event press/release events and fires clean pico_button_click events with click_type: single, double, or long.

Install it once, configure your timing preferences globally, and every Pico in your system gets double/long press support automatically. It sits alongside the core Caseta integration and doesn’t modify or replace anything.

How It Works

The component runs a state machine for each button:

  1. On press: if a timer is already running (previous press) → double click. Otherwise, start a timer.
  2. On release: if a long press already fired → consume the release. Otherwise, mark the release.
  3. On timer expire: if no release received → long press. If released → single press.

Repeat Behavior for Raise/Lower

Buttons in the slow_buttons list (typically raise and lower) get special handling:

  • Extra time window for double-click detection (slow_extra parameter)
  • Holding fires repeated long events at configurable intervals, perfect for stepping fan speed or brightness
  • All other buttons fire long press exactly once

Configuration

# configuration.yaml
pico_click:
  click_time: 0.5        # seconds, window to detect double click
  slow_extra: 0.25       # extra time for slow buttons (raise/lower)
  repeat_time: 0.5       # interval for long-press repeat
  repeat_max: 10         # max repeat count
  slow_buttons:          # buttons that get extra time AND repeat on long press
    - raise
    - lower

That’s it. No per-remote configuration. Every Pico on your Caseta bridge is covered.

Event Format

event_type: pico_button_click
data:
  device_id: 4db3aae61480b353222838233e4b8f1b
  device_name: Master Bedroom Pico
  area_name: Master Bedroom
  serial: 43195928
  type: Pico3ButtonRaiseLower
  button_number: 3
  button_type: stop
  click_type: double    # single, double, or long

Example Automation

One Pico controlling two devices. Single press for the ceiling fan, double press for a Dyson fan:

- id: master_bedroom_fan_control
  alias: Master Bedroom Fan Control
  trigger:
    - platform: event
      event_type: pico_button_click
      event_data:
        device_id: 4db3aae61480b353222838233e4b8f1b
  action:
    - variables:
        button: "{{ trigger.event.data.button_type }}"
        click: "{{ trigger.event.data.click_type }}"
    - choose:
        # Single press  - ceiling fan
        - conditions: "{{ button == 'on' and click == 'single' }}"
          sequence:
            - service: fan.turn_on
              target:
                entity_id: fan.ceiling_fan
        - conditions: "{{ button == 'off' and click == 'single' }}"
          sequence:
            - service: fan.turn_off
              target:
                entity_id: fan.ceiling_fan
        - conditions: "{{ button == 'raise' and click in ['single', 'long'] }}"
          sequence:
            - service: fan.increase_speed
              target:
                entity_id: fan.ceiling_fan
        - conditions: "{{ button == 'lower' and click in ['single', 'long'] }}"
          sequence:
            - service: fan.decrease_speed
              target:
                entity_id: fan.ceiling_fan
        # Double press  - Dyson fan
        - conditions: "{{ button == 'on' and click == 'double' }}"
          sequence:
            - service: fan.turn_on
              target:
                entity_id: fan.dyson_am07
        - conditions: "{{ button == 'off' and click == 'double' }}"
          sequence:
            - service: fan.turn_off
              target:
                entity_id: fan.dyson_am07

Comparison with Existing Solutions

Blueprint lutron-caseta-pro fork pico_click
Per-remote config Yes Yes No
Replaces core integration No Yes No
Raise/lower double-press Disabled Supported Supported with tunable timing
Long-press repeat No No Yes, configurable
Event-based No (inline actions) Custom codes Clean typed events

Installation

  1. Create the directory /config/custom_components/pico_click/
  2. Create the two files below
  3. Add the configuration block to configuration.yaml
  4. Restart Home Assistant

/config/custom_components/pico_click/manifest.json

{
  "domain": "pico_click",
  "name": "Pico Click",
  "version": "1.0.0",
  "documentation": "https://github.com/rnilssoncx/homebridge-pico",
  "dependencies": [],
  "codeowners": [],
  "iot_class": "local_push"
}

/config/custom_components/pico_click/__init__.py

"""Pico Click - Single, double, and long press detection for Lutron Pico remotes.

Ported from homebridge-pico by Robert Nilsson.
Listens for raw lutron_caseta_button_event press/release events and fires
pico_button_click events with click_type: single, double, or long.
"""

import asyncio
import logging

from homeassistant.core import HomeAssistant, Event
from homeassistant.helpers.typing import ConfigType

_LOGGER = logging.getLogger(__name__)

DOMAIN = "pico_click"
EVENT_PICO_CLICK = "pico_button_click"
EVENT_CASETA = "lutron_caseta_button_event"

DEFAULT_CLICK_TIME = 0.5
DEFAULT_SLOW_EXTRA = 0.25
DEFAULT_REPEAT_TIME = 0.5
DEFAULT_REPEAT_MAX = 10
DEFAULT_SLOW_BUTTONS = ["raise", "lower"]


class ClickTracker:
    """State machine for detecting single, double, and long press.

    Ported from the Click class in homebridge-pico index.js.

    Logic:
      - On press: if timer running -> double click, else start timer
      - On release: if long press already fired, stop everything
      - On timer expire: if no release -> long press, else -> single press
      - Repeat only fires for buttons in the repeat list (raise/lower)
    """

    def __init__(self, hass, event_data, click_time, repeat_time, repeat_max, repeat, callback):
        self._hass = hass
        self._device_id = event_data["device_id"]
        self._button_type = event_data["button_type"]
        self._event_data = event_data
        self._click_time = click_time
        self._repeat_time = repeat_time
        self._repeat_max = repeat_max
        self._repeat = repeat
        self._callback = callback
        self._timer = None
        self._ups = 0
        self._repeat_count = 0
        self._long_fired = False
        self._released = False

    async def click(self, action):
        if action == "press":
            self._long_fired = False
            self._released = False
            if self._timer is not None:
                await self._finished(double_click=True)
            else:
                self._set_timer(self._click_time)
        elif action == "release":
            self._released = True
            if self._long_fired:
                if self._timer is not None:
                    self._timer.cancel()
                    self._timer = None
                self._repeat_count = 0
                return
            if self._timer is not None:
                self._ups += 1

    def _set_timer(self, seconds):
        if self._timer is not None:
            self._timer.cancel()
        self._timer = self._hass.loop.call_later(
            seconds, lambda: asyncio.ensure_future(self._finished())
        )
        self._ups = 0

    async def _finished(self, double_click=False):
        if self._timer is not None:
            self._timer.cancel()
            self._timer = None

        if self._released and self._long_fired:
            self._repeat_count = 0
            return

        if double_click:
            click_type = "double"
            self._repeat_count = 0
        elif self._ups == 0:
            click_type = "long"
            self._long_fired = True
            if self._repeat:
                self._repeat_count += 1
                if self._repeat_count < self._repeat_max and not self._released:
                    self._set_timer(self._repeat_time)
                else:
                    self._repeat_count = 0
        else:
            click_type = "single"
            self._repeat_count = 0

        if self._repeat_count == 0 or click_type == "long":
            await self._callback(self._event_data, click_type)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
    """Set up the Pico Click component."""
    conf = config.get(DOMAIN, {})
    click_time = conf.get("click_time", DEFAULT_CLICK_TIME)
    slow_extra = conf.get("slow_extra", DEFAULT_SLOW_EXTRA)
    repeat_time = conf.get("repeat_time", DEFAULT_REPEAT_TIME)
    repeat_max = conf.get("repeat_max", DEFAULT_REPEAT_MAX)
    slow_buttons = conf.get("slow_buttons", DEFAULT_SLOW_BUTTONS)

    trackers = {}

    async def fire_click(event_data, click_type):
        """Fire a pico_button_click event."""
        data = {
            "device_id": event_data.get("device_id", ""),
            "device_name": event_data.get("device_name", ""),
            "area_name": event_data.get("area_name", ""),
            "serial": event_data.get("serial", ""),
            "type": event_data.get("type", ""),
            "button_number": event_data.get("button_number", ""),
            "button_type": event_data.get("button_type", ""),
            "click_type": click_type,
        }
        _LOGGER.debug("Pico click: %s %s %s", data["device_name"], data["button_type"], click_type)
        hass.bus.async_fire(EVENT_PICO_CLICK, data)

    async def handle_caseta_event(event: Event):
        """Handle raw Caseta button events."""
        data = event.data
        action = data.get("action")
        if action not in ("press", "release"):
            return

        device_id = data.get("device_id", "")
        button_type = data.get("button_type", "")
        key = f"{device_id}_{button_type}"

        if key not in trackers:
            button_click_time = click_time
            repeat = button_type in slow_buttons
            if repeat:
                button_click_time += slow_extra
            trackers[key] = ClickTracker(
                hass, data, button_click_time, repeat_time, repeat_max, repeat, fire_click
            )
        else:
            trackers[key]._event_data = data

        await trackers[key].click(action)

    hass.bus.async_listen(EVENT_CASETA, handle_caseta_event)
    _LOGGER.info(
        "Pico Click loaded (click_time=%.2fs, slow_extra=%.2fs, repeat_time=%.2fs, repeat_buttons=%s)",
        click_time, slow_extra, repeat_time, slow_buttons,
    )
    return True

Background

I originally built homebridge-pico, a Homebridge plugin that added double and long press to Pico remotes via the Caseta Telnet protocol. It’s been running reliably in my setup for years and I wanted the same simplicity and reliability when I moved to Home Assistant.

The existing HA solutions (per-remote blueprints and integration forks) felt like a step backwards. I wanted what homebridge-pico gave me: install once, configure timing globally, and every Pico just works. pico_click brings that same approach to HA as a lightweight custom component that sits alongside the core Caseta integration.

Key adaptation from Homebridge: The original plugin used Caseta Telnet protocol action codes (3=down, 4=up). HA’s Caseta integration sends action: press and action: release with string button_type values (on, off, raise, lower, stop) instead of numeric button IDs. The state machine logic is the same, only the event interface is different.

To find the correct button_type for any Pico, look at Settings > Devices & Services > Lutron Caseta > [device] > Controls. The button entity names match the button_type values in events.

1 Like