- Deploy v0.3.0 (main 216cc99, W1N-180 bot-initiated device verification wizard) on hass.windy.lan; backup matrix_e2ee.bak-20260818-v0.2.10 - Fix runbook: tag-only prerequisite (v0.3.0 was untagged), working-tree HEAD check, rsync exit-23 note, actual setup log line, post-deploy record step, ssh_config.d -F /dev/null gotcha - Rename runbook matrix-e2e-update.md -> matrix-e2ee-update.md and update AGENTS.md/hosts references (domain is matrix_e2ee, double-e) - Unify matrix_e2ee naming and update version history in docs/home-assistant-matrix.md
317 lines
14 KiB
Markdown
317 lines
14 KiB
Markdown
# 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.0** (W1N-180/#32: bot-initiated device verification wizard),
|
||
deployed on `hass.windy.lan` 2026-08-18 (upgraded from v0.2.10).
|
||
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)
|
||
- `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 v0.3.0 wizard automates
|
||
the bot-initiated 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.0**: bot-initiated device verification wizard (W1N-180/#32).
|
||
- Inbound SAS is gated to `allowed_users` (v0.1.3).
|
||
|
||
### 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)
|