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>
110 lines
5.1 KiB
Markdown
110 lines
5.1 KiB
Markdown
# `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 —
|
|
|
|
```python
|
|
{"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
|
|
|
|
```bash
|
|
./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:
|
|
|
|
```yaml
|
|
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
|
|
|
|
```bash
|
|
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"}'
|
|
```
|