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

16 KiB
Raw Permalink Blame History

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 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 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 (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. 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 — 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):

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_e2ee (custom E2E integration)

Custom integration 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 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, matrix-nio examples, element-android#6832, Element 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 usersin-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.requestm.key.verification.ready since nio lacks a request framework (W1N-173).
  • v0.2.5: bridge m.key.verification.requestready (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