# 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 `!`. 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 (`!: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: 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: - 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)