Skip to content
Get the appGet app

Buttons and Toggles

Some templates have buttons and toggles you can press. Each press goes to your own system, which carries it out and publishes the new state back.

TemplateWhere you press
Switchboard widgetUp to 8 tiles on the Home Screen.
Remote widgetUp to 5 buttons on the Home Screen.
Remote Live ActivityUp to 3 buttons on the Lock Screen and in the expanded Dynamic Island.

Everything on this page applies to all three.

A button or toggle is an action in the content you publish. Every action has an id, which is what your system receives when it is pressed, and a kind:

  • A button does something once: start a scene, run a script, open the garage. Its optional active flag is a highlight that only you set, for example on the scene that is running now.
  • A toggle has two states: a light, a fountain, a fan. Its on value says whether the thing is on right now. A press asks for a change, and your next update says whether it happened.
  • A link opens a URL: a Home Assistant dashboard, a camera app, a web page. Nothing is sent to your system. See Links. Remote widgets and Remote Live Activities only.

The widgets share the action object; the Live Activity takes a subset of it.

A link is an action with "kind": "link" and a url. Pressing it opens the URL, the same way a tap action URL does:

{"id": "camera", "kind": "link", "label": "Camera", "icon": "video.fill",
"url": "homeassistant://navigate/lovelace/garage"}
  • Nothing reaches your system. A link sends no event, calls no webhook, waits for nothing and doesn’t count toward the rate limit. It has no on, active, state_text or confirm.
  • Looks like a button at rest, with a small arrow in the corner that says the tap leaves the widget. It never shows Sent or Turning on.
  • The URL is at most 256 characters, with any scheme a phone can open: https, http, tel, sms, facetime, or an app’s own, such as homeassistant://. javascript, data, file, blob, about and aivi URLs are rejected. The same rule applies to tap_url.
  • Remote only, for now. The Remote widget and the Remote Live Activity take links. A Switchboard rejects them until enough phones run a version of Aivi that understands them.
  • Shared widgets and activities ask first. Your own links open straight away. When someone you shared with taps a link, or a tap_url, Aivi asks “Open ha.example.com?” and shows the full URL. They can choose Always Allow, which Aivi remembers on that phone only, in Settings › Links:
    • For a website, Always Allow covers that address: the scheme, the host and a non-default port, such as https://ha.example.com.
    • For an app link, it covers the whole app: Always Allow on a shortcuts:// link opens every shortcuts:// link on items shared with them, whatever it does, and the question says so.
    • tel, sms and facetime links always ask, with Open and Cancel only. The phone confirms calls itself.
  • Needs Aivi 2.4.
  1. You publish the content, with the actions and whether each toggle is on: PATCH /widgets/{slug} or PATCH /activities/{slug}.
  2. Someone presses a button or toggle. The phone sends the press to Aivi, and shows that it is waiting.
  3. Aivi hands the press to your system as an event, over your event stream or a webhook.
  4. Your system carries it out, for example turns the porch light on.
  5. Your system publishes the new state: the porch light’s toggle with "on": true. The phone that sent the press shows it at once, and every other phone updates too.

The phone never decides on its own that something is on: a toggle only looks on when your system says so.

Buttons and toggles show what you published, plus a clear sign while a press is on its way. Filled means on: a toggle that is on, or a button you published as "active": true.

StateWhat it shows
Off (toggle), idle (button)A neutral button.
On (toggle), active (button)Filled with its color, or on a tile with its gradient or picture.
Turning on, turning offAt once after the press: a light wash of the color it will fill with, or the fill fading out.
No responseYour update didn’t arrive within 15 seconds. The toggle goes back to the state you last published, marked No response for about a minute.
Sent (button)A check over a light wash until your update arrives, for at most about 7 seconds. It means the press reached Aivi, not that the scene ran.
A medium Switchboard with presses in flight: Garage door with a faint orange wash reading Opening, Movie with a check in its gradient mark reading Sent, Evening filled with its sunset gradient reading Active, Porch light back to off reading No response in orange, Dog cam on with its picture, and Fountain with a faint teal wash reading Turning off.

Switchboard tiles name the state on the second line under the label, and can use your own words, such as Opening for a garage door: see State words. Smaller buttons, on a Remote widget or a Live Activity, show a warning badge for No response; a Live Activity also says it in the card’s state line.

Only toggles show No response: a button has no state to confirm, so an unanswered press just ends. If a press can’t be sent at all, for example without a connection, it goes straight back to what it showed before.

When your system has carried a press out, publish the content again with what is true now: the toggle’s on, or the button’s active. Content replaces the previous content as a whole, so send every action, not only the one that changed:

curl https://api.getaivi.app/widgets/living-room \
-X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Token YOUR_API_TOKEN" \
--data-binary @- << 'EOF'
{
"content": {
"template": "switchboard",
"actions": [
{ "id": "movie", "label": "Movie", "icon": "tv.fill", "active": true },
{ "id": "porch", "label": "Porch light", "icon": "lightbulb.fill", "kind": "toggle", "on": true }
]
}
}
EOF
  • A toggle is confirmed by an update that shows it in the requested state.
  • A button is confirmed by any update after the press, even one identical to the last. Publish after every button press, so it stops showing Sent as soon as the press is handled.
  • A failed press needs no answer. If the light stayed off, keep it published as off: after 15 seconds the toggle shows No response over the state you published.

Answer quickly: the phone that sent the press waits up to 15 seconds for a toggle and 5 for a button. If you already publish whenever the device changes, you need no extra code: the change the press caused triggers the confirming update.

On other phones, a widget refreshes when iOS allows, and Aivi sends at most one update per widget every 2 seconds. A Live Activity updates at once everywhere.

A Live Activity takes presses only while it is running:

  • Confirm with "state": "ONGOING". An update with IDLE ends the card, along with any press still waiting on it.
  • Presses belong to one card. Each run, from the ONGOING that starts it to the IDLE that ends it, is a new card, and every press carries its card’s instance_id. A press on a card that has ended is never delivered, and the phone removes that card.
  • Long-running activities get new cards too. When Aivi refreshes an activity past the system’s time limit, presses on the old card stop.
  • Updates count. An update that changes the content is a push, and counts toward the activity’s update budget. Republishing identical content still confirms a button, without a push.
  • Needs Aivi 2.4. Phones on an older version don’t show the activity.

The owner and everyone the widget or activity is shared with can press it. Every press is delivered to the owner’s account: the owner’s API token opens the event stream, and the owner configures the webhooks.

Widget presses come from the Home Screen. The Lock Screen and Apple Watch versions of a widget, a widget inside a Board, and the previews in the app show the state but can’t be pressed. When a widget hides its content on a locked phone, it takes no presses until the phone is unlocked.

Presses are rate limited, so a button tapped over and over can’t flood your system:

  • Per phone and widget or activity: 8 presses in a quick burst, then one a second, and at most 60 a minute.
  • Per account: 20 presses in a quick burst across all your widgets, activities and the phones that show them, then one a second.

A press over the limit is refused and never reaches you, and the phone ignores taps on it for a moment. A press the phone retries, for example after a timeout, is delivered only once.

Delivery to your system can still repeat: a webhook is retried after a transient failure, and a stream that reconnects can see an event again. Every press has its own event id, the same on every retry, so deduplicate on it when an action isn’t safe to repeat. Turning a light on twice does no harm; feeding the cat twice might.

The Aivi integration sets up Switchboard and Remote buttons from a panel in Home Assistant: choose the entities behind them, and presses reach Home Assistant over a connection it opens itself, with no webhook, no open port and no Home Assistant Cloud. The rest of this section builds a switchboard with your own automations.

The example publishes a switchboard with two scenes and two toggles, and turns presses into service calls. It reuses the update_widget RESTful command from the widget templates page; if you haven’t published a widget from Home Assistant yet, the Home Assistant widgets guide sets it up step by step. Create a widget with the Switchboard template in the app first, with the slug living-room.

  1. Publish the switchboard whenever one of its entities changes. Every update carries the whole content, with each toggle’s on read from its entity:

    - alias: "Aivi: publish the living room switchboard"
    triggers:
    - trigger: state
    entity_id:
    - light.porch
    - switch.fountain
    - trigger: homeassistant
    event: start
    actions:
    - action: rest_command.update_widget
    data:
    slug: living-room
    payload: >-
    {{ {"content": {"template": "switchboard", "actions": [
    {"id": "movie", "label": "Movie", "icon": "tv.fill"},
    {"id": "all_off", "label": "All off", "icon": "power"},
    {"id": "porch", "label": "Porch light", "icon": "lightbulb.fill",
    "kind": "toggle", "color": "yellow", "on": is_state("light.porch", "on")},
    {"id": "fountain", "label": "Fountain", "icon": "drop.fill",
    "kind": "toggle", "color": "cyan", "on": is_state("switch.fountain", "on")}
    ]}} | tojson }}

    This automation is also what confirms a press: turning the porch light on changes its state, and the update fills in the tile. A scene changes none of these entities, so the next step runs this automation after each scene or script to answer the button press.

  2. Make a token of your own. Aivi sends it with every press, and the automation in the next step checks it, so that only Aivi can use the webhook:

    openssl rand -hex 32
  3. Receive presses with a webhook trigger. It checks the token, drops expired presses, then carries out the action:

    - alias: "Aivi: living room switchboard presses"
    mode: queued
    max: 10
    triggers:
    - trigger: webhook
    webhook_id: "[webhook-id]"
    allowed_methods: [POST]
    local_only: true
    conditions:
    - condition: template
    value_template: "{{ trigger.json.token == '[shared-secret]' }}"
    actions:
    - variables:
    event: "{{ trigger.json.event | from_json }}"
    - condition: template
    value_template: >-
    {{ event.type == 'widget.action.v1'
    and as_timestamp(event.expires_at, 0) > as_timestamp(now()) }}
    - choose:
    - conditions: "{{ event.data.action == 'movie' }}"
    sequence:
    - action: scene.turn_on
    target: { entity_id: scene.movie }
    - action: automation.trigger
    target: { entity_id: automation.aivi_publish_the_living_room_switchboard }
    - conditions: "{{ event.data.action == 'all_off' }}"
    sequence:
    - action: script.turn_on
    target: { entity_id: script.all_off }
    - action: automation.trigger
    target: { entity_id: automation.aivi_publish_the_living_room_switchboard }
    - conditions: "{{ event.data.kind == 'toggle' }}"
    sequence:
    - variables:
    entity: >-
    {{ {"porch": "light.porch", "fountain": "switch.fountain"}[event.data.action] }}
    - if: "{{ event.data.value }}"
    then:
    - action: homeassistant.turn_on
    target: { entity_id: "{{ entity }}" }
    else:
    - action: homeassistant.turn_off
    target: { entity_id: "{{ entity }}" }
  4. Point the widget’s webhooks at it. The "*" entry catches every action, and the body template carries your token and the whole event:

    curl https://api.getaivi.app/widgets/living-room/webhooks \
    -X PUT \
    -H "Content-Type: application/json" \
    -H "Authorization: Token [token]" \
    -d '{"webhooks": {"*": {"url": "[webhook-url]", "body": {"token": "[shared-secret]", "event": "{{event_json}}"}}}}'

The same recipe drives a Remote widget: publish "template": "remote" content instead. For a Remote Live Activity, publish with a rest_command for PATCH /activities/{slug} with "state": "ONGOING", check for activity.action.v1 in step 3, and set the webhooks on /activities/{slug}/webhooks in step 4.

Presses need a connection, expire 5 seconds after they happen, and are delivered only to public HTTPS webhooks or an open event stream. See Limitations for the full list.