Files
vps/docs/home-assistant-matrix.md
T

346 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Home Assistant × Matrix integration
Reference for wiring the Home Assistant [Matrix integration](https://www.home-assistant.io/integrations/matrix)
to the self-hosted Matrix homeserver at [`synapse.chans.xyz`](../hosts/synapse.chans.xyz.md).
Deliberately contains no Matrix passwords, access tokens, or room encryption material.
> **Status (2026-08-15, W1N-139):** the built-in `matrix` integration has been
> **retired** on `hass.windy.lan` and replaced by the custom **`matrix_e2ee`**
> integration. The sections below on the built-in integration are kept for
> reference only. See [matrix_e2ee](#matrix-e2ee-custom-e2e-integration) for the
> active setup and [Device verification (SAS) model](#device-verification-sas-model)
> for how device trust works.
## Purpose
The integration lets Home Assistant send messages to Matrix rooms and react to
messages/reactions in Matrix rooms. "Reacting" is done by firing a
`matrix_command` event when one of the configured commands matches; automations
then trigger on that event. Sending is done through the `notify.matrix` platform
and the `matrix.send_message` / `matrix.react` actions.
## Environment mapping
| Integration setting | This deployment |
|---|---|
| `homeserver` | `https://synapse.chans.xyz` (client-server base URL) |
| `username` | full Matrix ID, e.g. `@ha_bot:chans.xyz` |
| `password` | MAS local-password account password (see below) |
| Room IDs / aliases | full forms with the identity domain, e.g. `!cUrbafjkfsMDVwdRDQ:chans.xyz` or `#room:chans.xyz` |
- Identity domain is `chans.xyz` (not `synapse.chans.xyz`); user IDs and room
aliases carry the `:chans.xyz` suffix.
- Authentication on this homeserver is MAS (Matrix Authentication Service) with
local-password accounts. The integration logs in with `m.login.password`
(username + password), so the bot account must be a local-password account —
same as the Hermes account documented in [`hermes-matrix.md`](hermes-matrix.md).
If MAS is later switched to OAuth2/OIDC-only (no legacy password login), the
integration's password login will stop working; keep that in mind before such
a change.
- Public registration is disabled. Create/reset the dedicated bot account via
MAS / Element Admin.
## Use a separate bot account (mandatory)
The docs are explicit: to prevent infinite loops when reacting to commands,
the integration **must** use a separate account from any account whose messages
it reacts to. Use a dedicated account such as `@ha_bot:chans.xyz`, not a human
account.
## configuration.yaml (example)
```yaml
# The Matrix integration
matrix:
homeserver: https://synapse.chans.xyz
username: "@ha_bot:chans.xyz"
password: supersecurepassword
rooms:
- "#hasstest:chans.xyz"
commands:
- word: my_command
name: my_command
```
After changing `configuration.yaml`, restart Home Assistant to apply the
changes. The integration then shows under **Settings → Devices & services**;
its entities are on the integration card and the Entities tab.
### Configuration variables
| Variable | Meaning |
|---|---|
| `username` | Full Matrix ID the bot logs in as, e.g. `@ha_bot:chans.xyz`. The `@` has a special YAML meaning, so always quote it. |
| `password` | The bot account's password (MAS local password). |
| `homeserver` | Full client-server URL of the homeserver. |
| `rooms` | Rooms the bot should join and listen in. List **all** rooms commands are to be received in, even if a command scopes itself to fewer rooms. Accepts internal room ID (`!…:chans.xyz`) or alias (`#room:chans.xyz`). |
| `commands` | Commands to listen for. Each fires a `matrix_command` event when triggered. |
### Command types
| Key | Triggers when |
|---|---|
| `word` | A message starts with `!<word>`. Arguments after the word are captured as a list in the event's `data`. |
| `expression` | A message matches the Python regexp. The regexp group dictionary is captured in the event's `data`. |
| `reaction` | A message is reacted to with the given emoji. |
| `name` | The command name, exposed as an attribute of the fired event. |
A command can be scoped to specific rooms with a per-command `rooms` list (the
room must still be listed under the top-level `rooms`).
## Event data
When a command triggers, a `matrix_command` event fires with:
- `name` — the command name.
- `data` — for `word` commands, a list of arguments (everything after the word,
split on spaces); for `expression` commands, the group dictionary of the
matching regexp.
- `event_id` — the received message's identifier.
- `thread_parent` — the root message ID of the thread; equals `event_id` when
the message is not inside a thread.
## Notifications (notify.matrix)
Deliver notifications from Home Assistant to a Matrix room (direct or group):
```yaml
notify:
- name: matrix_notify
platform: matrix
default_room: "#hasstest:chans.xyz"
```
- The target room must already exist; get its canonical ID from the room
settings dialog (`!<randomid>:chans.xyz`) or an alias (`#roomname:chans.xyz`).
Quote the room ID/alias in YAML to escape the `!` / `#` characters.
- The notifying account may need to be invited to the room, depending on room
policy.
Message formats (`data.format`): `text` (default) and `html`. Images can be
attached via `data.images` (list of file paths); files from outside allowed
folders require `homeassistant.allowlist_external_dirs` to list the source
folder.
Reply inside a thread by passing the root message ID into `data.thread_id`:
```yaml
action: notify.matrix_notify
data:
message: "Reply message goes here"
data:
thread_id: "{{ trigger.event.data.thread_parent }}"
```
## Actions
- `matrix.react` — send a reaction to a message in a Matrix room
(`reaction`, `room`, `message_id`).
- `matrix.send_message` — send a message to one or more Matrix rooms.
## Comprehensive example (adapted)
```yaml
matrix:
homeserver: https://synapse.chans.xyz
username: "@ha_bot:chans.xyz"
password: supersecurepassword
rooms:
- "#hasstest:chans.xyz"
- "#someothertest:chans.xyz"
commands:
- word: testword
name: testword
rooms:
- "#someothertest:chans.xyz"
- expression: "My name is (?P<name>.*)"
name: introduction
- reaction: 👍
name: thumbsup
notify:
- name: matrix_notify
platform: matrix
default_room: "#hasstest:chans.xyz"
automation:
- alias: "Respond to !testword"
triggers:
- trigger: event
event_type: matrix_command
event_data:
command: testword
actions:
- action: notify.matrix_notify
data:
message: "It looks like you wrote !testword"
```
## matrix_e2ee (custom E2E integration)
Custom integration [`windyboy/ha-matrix-e2ee`](https://github.com/windyboy/ha-matrix-e2ee),
release **v0.3.12** (Matrix activity events + push diagnostics), deployed on
`hass.windy.lan` 2026-08-20 (upgraded from v0.3.2, W1N-182/#34 emoji-wait
wizard fix; v0.3.9 brought the Connection health binary sensor, SAS/command
allowlist split, URL normalization and single-entry enforcement, W1N-156/W1N-190).
Runs a dedicated bot with a **persistent E2EE device identity**.
- Domain `matrix_e2ee`; Config Flow (UI) with YAML import migration, not in HACS. Does **not**
override the built-in `matrix` integration.
- Dependencies are declared **explicitly** in `manifest.json` to work around Home
Assistant's `is_installed` dropping the `[e2e]` extra (W1N-140):
`matrix-nio[e2e]==0.26.0` + `vodozemac` + `peewee` + `cachetools` + `atomicwrites`.
- **v0.2.0 migration:** YAML `matrix_e2ee:` block was auto-imported into a Config Entry
(`source: import`) on first startup, then removed. All settings now managed via
**Settings → Devices & Services → Matrix E2EE → Configure**.
See [hosts/hass.windy.lan.md](../hosts/hass.windy.lan.md) for the deployed state.
### Services & events
- Services (all admin-only since v0.1.4):
- `send_message` (`message`, `room_id`)
- `start_verification` (`user_id`, `device_id`)
- `confirm_verification` (`transaction_id`)
- `cancel_verification` (`transaction_id`)
- `reauthenticate` (`password`) — soft-logout only
- `get_fingerprint` (no fields; returns bot's own `ed25519`/`curve25519` keys; added v0.1.3)
- `verify_device_by_fingerprint` (`user_id`, `device_id`, `ed25519`; added v0.1.3,
renamed from `verify_device` in v0.1.4; requires exact `ed25519` match)
- Events:
- `matrix_e2ee_command` (`room_id`, `sender`, `command`, `args` only —
never the raw body)
- `matrix_e2ee_error` (codes, no secrets)
- `matrix_e2ee_verification` (`stage`, `transaction_id`, `user_id`, `device_id`,
optional `emojis`, optional `expires_at`; `expires_at` added v0.1.3)
- `matrix_e2ee_fingerprint` (`user_id`, `device_id`, `ed25519`, `curve25519`
public keys only; added v0.1.3)
- `matrix_e2ee_message_received` (`room_id`, `sender`, `event_id`; added v0.3.12
activity events)
- `matrix_e2ee_verification_done` (`transaction_id`, `user_id`, `device_id`;
added v0.3.12)
- v0.3.12 also adds an `event.` platform entity (`Bot activity`,
`event_types: ["message", "command", "verification_done"]`) and a diagnostic
Connection binary sensor (`binary_sensor.*_connection`, CONNECTIVITY class).
- `notify.matrix_e2ee` is **not implemented** (upstream deferred) — notifications
must call `matrix_e2ee.send_message` (message + room_id).
- Commands fire Home Assistant events only; the integration never calls
`domain.service` itself. Map commands in automations.
- Encrypted rooms fail-closed on unverified devices.
- Since v0.1.4: `start_verification`, `confirm_verification`, `cancel_verification`,
`verify_device_by_fingerprint`, and `reauthenticate` are enforced as HA admin-only
via `async_register_admin_service`; non-admin users cannot call them.
### Storage & recovery
- `.storage/matrix_e2ee_session.json` (`user_id`, `device_id`, `access_token`,
`pickle_key`) and `.storage/matrix_e2ee_store/` (Olm/Megolm, device trust,
sync token). Both stay on the HA persistent volume and are in HA backups.
- Soft logout → `matrix_e2ee.reauthenticate` (keeps `device_id` + crypto store;
rejected outside soft-logout state since v0.1.3).
- Hard logout / store loss → delete session + store, restart with password, re-SAS
(a **new device**; old history not decryptable).
## Device verification (SAS + fingerprint) model
Researched 2026-08-15 (W1N-139 stage-6 pre-study), updated for v0.1.3/v0.1.4.
Sources: matrix.org
[cross-signing guide](https://matrix.org/docs/guides/implementing-more-advanced-e-2-ee-features-such-as-cross-signing/),
matrix-nio [examples](https://matrix-nio.readthedocs.io/en/latest/examples.html),
[element-android#6832](https://github.com/vector-im/element-android/issues/6832),
Element [device-verification](https://element.io/features/device-verification).
`matrix_e2ee` supports three verification paths (the wizard — v0.3.0
bot-initiated, reworked in v0.3.1/v0.3.2 to wait for a peer-initiated inbound
SAS from the user's Matrix client with emoji comparison — automates the SAS
flow):
### 1. SAS (mutual, manual confirmation since v0.1.4)
- SAS is device-to-device: exchange ephemeral keys → derive emojis → **a human on
each side compares and confirms** (`m.key.verification.mac`).
- Matrix distinguishes two cases (spec uses *should*, not *must*):
- **same user, two devices** → to-device messages (SAS);
- **two different users** → **in-room (DM) messages**, verifying the *user*
(cross-signing master key), not a specific device.
- Cross-signing: each user has master / self-signing / user-signing keys. A device
looks "verified" to another user via the chain
`my master → my user-signing → their master → their self-signing → their device`.
- Element's "Verify" button only starts **in-DM user verification**; it has no
"verify a specific device of another user via to-device" flow (matrix.org
recommends hiding per-device verification for other users).
- `matrix_e2ee` implements **raw to-device device SAS** (`start_verification`/
`confirm_verification`), **no cross-signing / in-room**. This is a non-standard
cross-user path: works with matrix-nio + Element Web/Desktop (reported in
element-android#6832), **not** on Element Android/X.
- **v0.1.3**: inbound SAS auto-complete was added; SAS events include `expires_at`.
- **v0.1.4 (breaking)**: auto-confirm was removed. **Every** device — including
another device of the bot's own account — requires explicit `confirm_verification`
after emoji comparison. Only the bot's own account or users in `allowed_users`
may initiate SAS (`verification_peer_denied` otherwise).
- **v0.2.1**: storage I/O moved off the event loop (`asyncio.to_thread`,
W1N-167); own-keys query on startup so inbound SAS can build a session (W1N-166).
- **v0.2.2** (not deployed): intermediate version.
- **v0.2.3**: sync loop runs as a background task (fixes bootstrap setup timeout,
W1N-168); SAS double-send of key and MAC fixed (W1N-169).
- **v0.2.6**: `_log_verification_state()` tracks SAS state transitions with
`async_write_ha_state` for diagnosis (W1N-174);
`_bridge_verification_request()` handles inbound
`m.key.verification.request``m.key.verification.ready` since nio lacks a
`request` framework (W1N-173).
- **v0.2.5**: bridge `m.key.verification.request``ready` (nio lacks
request framework, W1N-173).
- **v0.2.4**: `_patch_nio_sas_timeout()` works around nio 0.26.0
`_last_event_time` bug (SAS timed out at 60s regardless of activity — now uses
`_max_age` 5 min); `_repair_dropped_start()` recovers SAS `start` events nio
dropped when the peer device was unknown (W1N-170/W1N-172);
`VERIFICATION_TIMEOUT_SECONDS` 600→240 (fires before nio's `_max_age`).
- **v0.2.11**: `receive_mac_event` no longer overrides canceled state (W1N-179/#31).
- **v0.3.12**: Matrix activity events (`matrix_e2ee_message_received`,
`matrix_e2ee_verification_done`) + `event.` Bot activity entity + Connection
diagnostic binary sensor.
- **v0.3.9**: SAS driver gate split from the command allowlist — new
`verification_peer_users` option (W1N-156/#41); SAS/sync logs demoted
warning→info/debug (W1N-188/#38); Connection health binary sensor
(W1N-185/#40); URL normalization + single-entry enforcement (W1N-190/#42).
- **v0.3.8**: `m.key.verification.done` handshake completion for
request-based SAS (W1N-183/#35).
- **v0.3.2**: wizard waits for the inbound SAS to show emojis before moving
to the compare step (`_wait_for_inbound` requires `latest_sas_snapshot()` to
return `emojis`) — W1N-182/#34.
- **v0.3.1**: verification wizard now waits for a peer-initiated inbound SAS
(options flow no longer starts verification from the bot; `latest_sas_snapshot()`
skips verified/canceled transactions) — GitHub #33.
- **v0.3.0**: bot-initiated device verification wizard (W1N-180/#32).
- Inbound SAS is gated to `allowed_users` (v0.1.3); **since v0.3.9 (W1N-156)
the gate is the separate `verification_peer_users` allowlist**, which is
unset on hass.windy.lan — only the bot's own account may drive SAS until
`@zhiqiang:chans.xyz` is added there.
### 2. One-sided fingerprint (added v0.1.3, hardened v0.1.4)
- Call `matrix_e2ee.get_fingerprint` to get the bot's own `ed25519` device key
(read it from the `matrix_e2ee_fingerprint` event).
- In Element, open the bot user's sessions and use "Manually verify by text".
Compare the session key with the fingerprint.
- To trust another device from the bot's side, call
`matrix_e2ee.verify_device_by_fingerprint` with the peer's `user_id`, `device_id`,
and `ed25519` key. The match is exact (since v0.1.4's rename from `verify_device`).
Feed the **peer** key, not the bot's own key.
- This trusts from one side only; the peer still trusts the bot independently.
- Both `get_fingerprint` and `verify_device_by_fingerprint` are HA admin-only.
### Consequence
Whether `@zhiqiang`'s device can be verified depends on which Element client
they use. Open options recorded in W1N-139 (A: Web SAS test; B: upstream
in-room/cross-signing; C: unencrypted-room downgrade).
## References
- Home Assistant Matrix integration: <https://www.home-assistant.io/integrations/matrix>
- Matrix host facts: [`hosts/synapse.chans.xyz.md`](../hosts/synapse.chans.xyz.md)
- Matrix deployment and upstream index: [`matrix-upstream.md`](matrix-upstream.md)
- Hermes Agent Matrix channel (MAS local-password + access-token pattern): [`hermes-matrix.md`](hermes-matrix.md)
- HA host facts: [`hosts/hass.windy.lan.md`](../hosts/hass.windy.lan.md)
- HA maintenance runbook: [`runbooks/home-assistant-maintenance.md`](../runbooks/home-assistant-maintenance.md)