Files
Gabriel Vidal d4b2efb077 feat(ha-integration): open the installed PWA from notification taps (pwa_package)
The companion app turns an absolute https `clickAction` into a bare
ACTION_VIEW intent, which Android 12+ hands to the default browser — a
Chrome-minted WebAPK never has verified links for its host, so every
notify/ask tap opened a browser tab next to the installed app.

New optional `pwa_package` / `pwa_origin` config: a notification URL on the
PWA's origin is sent as an `intent:` URI pinned to the WebAPK's package
(path + query kept), which the app launches by package. Other hosts stay
plain links; empty package = previous behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 16:41:40 +02:00
..

ai_agent — Home Assistant custom integration

The Home Assistant side of the ai-agent notification hub. It registers a webhook (/api/webhook/ai_agent) that the ai-agent backend forwards its notify and ask events to, and turns them into Android companion-app notifications:

  • notify — a normal push on the per-type channel (Claude · Deploy, Claude · Error, …) with the same icon/color/vibration map the notify-done skill used to apply client-side. Explicit payload fields (icon, channel, vibrationPattern, …) always win over the type defaults.
  • ask — a persistent notification on the dedicated Claude · Ask channel with the ". .. .._" morse vibration pattern (0, 100, 350, 100, 120, 100, 350, 100, 120, 100, 120, 450) and one action button per option. When a button is tapped the integration POSTs {"index": n} back to the backend (POST /api/ask/{id}/answer) and clears the notification from the phone. Whatever is long-polling GET /api/ask/{id}?waitSecs=… (normally ask.sh) then unblocks with the chosen label.

Android locks a channel's importance/vibration when the channel is first created — to re-tune the ask buzz, rename ASK_CHANNEL in const.py (or clear the companion app's storage).

The voice leg (phone_service)

A notify has two destinations in the homelab: the screen (the Pixel push above) and the AI desk phone (services/phone, a Yealink that reads updates aloud over an auto-answer call). Both go through this one webhook: when phone_service: <domain>.<service> is configured, every notify is also handed to that HA service with the phone-relevant fields —

{"spoken": …, "type": …, "title": …, "message": …, "audio": …}

— non-blocking and best-effort, so a ringing (or absent) phone never delays the push nor fails the hub's webhook. The homelab points it at script.ai_agent_phone_notify (services/home-assistant/config/packages/ai_agent_phone.yaml), which POSTs to the phone bridge; that script is where HA-side routing policy belongs. Leave phone_service unset for screen-only notifications.

Asks have no voice leg on purpose — answering one means tapping a button.

Opening the installed PWA (pwa_package)

A notification's url is sent to the companion app as clickAction. The app turns an absolute http(s) URL into a bare ACTION_VIEW intent, and since Android 12 that lands in the default browser unless some app has verified links for the host — which a Chrome-installed PWA (a WebAPK) never has. So a tap on "deploy finished" opened a browser tab next to the installed app.

Set pwa_package to the WebAPK's package name and every URL on pwa_origin (default https://ai-agent.lab.gabvdl.xyz) is sent as an Android intent: URI pinned to that package instead:

intent://ai-agent.lab.gabvdl.xyz/conversation/<enc>/<sid>.jsonl?form=<id>#Intent;scheme=https;package=org.chromium.webapk.<hash>_v2;end

Path and query survive, so conversation links and ask-form deep links land in the app. URLs on any other host (a deployed site, a dashboard) stay plain links. Asks go through the same builder, so tapping an ask opens the app too.

Finding the package: install the PWA from Chrome (Brave only makes a home-screen shortcut, which has no package), open chrome://webapks on the phone and copy Package name (org.chromium.webapk.…). It is stable across PWA updates; a reinstall may mint a new one — update the config if taps start opening the Play Store, which is what the companion app does for a package it can't find. Leave pwa_package empty to get plain browser links back.

Events on the bus

Every handled payload is also fired on the HA event bus as ai_agent_notify / ai_agent_ask (the raw webhook JSON as event data), so automations can react to agent activity — flash a light on type: error, count deploys, mirror asks to another device — without re-implementing the webhook.

Install

./install.sh --restart     # copy component + config block, restart HA

HA_CONFIG overrides the target config dir (default: services/home-assistant/config). The config block it adds:

ai_agent:
  webhook_id: ai_agent                  # -> POST /api/webhook/ai_agent
  notify_target: mobile_app_pixel_9     # notify.<target>
  callback_url: http://127.0.0.1:8096   # ai-agent backend (host-published port)
  phone_service: script.ai_agent_phone_notify   # voice leg (see above)
  pwa_package: org.chromium.webapk.<hash>_v2    # optional: open the PWA (see above)
  pwa_origin: https://ai-agent.lab.gabvdl.xyz   # URLs on this origin → the PWA

HA runs on the host network, so 127.0.0.1:8096 reaches the ai-agent container's published port; the backend's trusted-caller gate accepts the connection because it arrives from the docker gateway.

Verify

docker logs home-assistant 2>&1 | grep ai_agent          # "ai_agent ready: …"
curl -s -X POST http://127.0.0.1:8123/api/webhook/ai_agent \
  -H 'Content-Type: application/json' \
  -d '{"event":"notify","title":"test","message":"hello","type":"success"}'