- Rename runbook: matrix-e2ee-update.md -> matrix-e2e-update.md - Update all references in AGENTS.md, hass.windy.lan.md, home-assistant-matrix.md to use the short name matrix_e2e - The code domain stays matrix_e2ee (E2EE) in source; all doc prose and command references now use matrix_e2e
14 KiB
Home Assistant × Matrix integration
Reference for wiring the Home Assistant Matrix integration
to the self-hosted Matrix homeserver at synapse.chans.xyz.
Deliberately contains no Matrix passwords, access tokens, or room encryption material.
Status (2026-08-15, W1N-139): the built-in
matrixintegration has been retired onhass.windy.lanand replaced by the custom **`matrix_e2e`** integration. The sections below on the built-in integration are kept for reference only. See matrix_e2e for the active setup and 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(notsynapse.chans.xyz); user IDs and room aliases carry the:chans.xyzsuffix. - 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 inhermes-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)
# 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— forwordcommands, a list of arguments (everything after the word, split on spaces); forexpressioncommands, the group dictionary of the matching regexp.event_id— the received message's identifier.thread_parent— the root message ID of the thread; equalsevent_idwhen the message is not inside a thread.
Notifications (notify.matrix)
Deliver notifications from Home Assistant to a Matrix room (direct or group):
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:
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)
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_e2e (custom E2E integration)
Custom integration windyboy/ha-matrix-e2e,
release v0.2.6 (W1N-173/W1N-174), deployed on hass.windy.lan
2026-08-16 (upgraded from v0.2.4; v0.2.0 was the first Config Flow release, W1N-154).
Runs a dedicated bot with a persistent E2EE device identity.
- Domain
matrix_e2e; Config Flow (UI) with YAML import migration, not in HACS. Does not override the built-inmatrixintegration. - Dependencies are declared explicitly in
manifest.jsonto work around Home Assistant'sis_installeddropping the[e2e]extra (W1N-140):matrix-nio[e2e]==0.26.0+vodozemac+peewee+cachetools+atomicwrites. - v0.2.0 migration: YAML
matrix_e2e: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 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 onlyget_fingerprint(no fields; returns bot's owned25519/curve25519keys; added v0.1.3)verify_device_by_fingerprint(user_id,device_id,ed25519; added v0.1.3, renamed fromverify_devicein v0.1.4; requires exacted25519match)
- Events:
matrix_e2e_command(room_id,sender,command,argsonly — never the raw body)matrix_e2e_error(codes, no secrets)matrix_e2e_verification(stage,transaction_id,user_id,device_id, optionalemojis, optionalexpires_at;expires_atadded v0.1.3)matrix_e2e_fingerprint(user_id,device_id,ed25519,curve25519— public keys only; added v0.1.3)
notify.matrix_e2eis not implemented (upstream deferred) — notifications must callmatrix_e2e.send_message(message + room_id).- Commands fire Home Assistant events only; the integration never calls
domain.serviceitself. 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, andreauthenticateare enforced as HA admin-only viaasync_register_admin_service; non-admin users cannot call them.
Storage & recovery
.storage/matrix_e2e_session.json(user_id,device_id,access_token,pickle_key) and.storage/matrix_e2e_store/(Olm/Megolm, device trust, sync token). Both stay on the HA persistent volume and are in HA backups.- Soft logout →
matrix_e2e.reauthenticate(keepsdevice_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, matrix-nio examples, element-android#6832, Element device-verification.
matrix_e2e supports two verification paths:
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_e2eimplements 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_verificationafter emoji comparison. Only the bot's own account or users inallowed_usersmay initiate SAS (verification_peer_deniedotherwise). - 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 withasync_write_ha_statefor diagnosis (W1N-174);_bridge_verification_request()handles inboundm.key.verification.request→m.key.verification.readysince nio lacks arequestframework (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_timebug (SAS timed out at 60s regardless of activity — now uses_max_age5 min);_repair_dropped_start()recovers SASstartevents nio dropped when the peer device was unknown (W1N-170/W1N-172);VERIFICATION_TIMEOUT_SECONDS600→240 (fires before nio's_max_age). - Inbound SAS is gated to
allowed_users(v0.1.3).
2. One-sided fingerprint (added v0.1.3, hardened v0.1.4)
- Call
matrix_e2e.get_fingerprintto get the bot's owned25519device key (read it from thematrix_e2e_fingerprintevent). - 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_e2e.verify_device_by_fingerprintwith the peer'suser_id,device_id, anded25519key. The match is exact (since v0.1.4's rename fromverify_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_fingerprintandverify_device_by_fingerprintare 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 - Matrix deployment and upstream index:
matrix-upstream.md - Hermes Agent Matrix channel (MAS local-password + access-token pattern):
hermes-matrix.md - HA host facts:
hosts/hass.windy.lan.md - HA maintenance runbook:
runbooks/home-assistant-maintenance.md