Files
Matrix-Server/custom_components/matrix_messenger/config_flow.py
T
Marc bddb07431d Initial release: Matrix Messenger Home Assistant integration
- Config flow (GUI): Homeserver, Passwort- oder Token-Anmeldung, Raumauswahl
- E2EE-Unterstützung via matrix-nio mit SQLite Key-Store
- Aktionen: send_message, ask_question (Text + Emoji-Reaktion)
- notify.*-Entitäten pro Raum
- Optionaler Hintergrund-Sync, 30-Min-Timeout für Fragen
- HACS-kompatibel, HA >= 2026.4.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 00:49:31 +02:00

327 lines
12 KiB
Python
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.
"""Config flow and options flow for Matrix Messenger."""
from __future__ import annotations
import logging
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.core import callback
from homeassistant.helpers import selector
from .const import (
AUTH_METHOD_PASSWORD,
AUTH_METHOD_TOKEN,
CONF_ACCESS_TOKEN,
CONF_AUTH_METHOD,
CONF_DEVICE_ID,
CONF_DEVICE_NAME,
CONF_ENABLE_SYNC,
CONF_HOMESERVER,
CONF_PASSWORD,
CONF_ROOMS,
CONF_USERNAME,
DEFAULT_DEVICE_NAME,
DOMAIN,
)
from .matrix_client import MatrixClient, MatrixClientError
_LOGGER = logging.getLogger(__name__)
class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Multi-step config flow: server → credentials → room selection."""
VERSION = 1
def __init__(self) -> None:
self._data: dict = {}
self._available_rooms: dict[str, str] = {}
# ------------------------------------------------------------------
# Step 1: homeserver + auth method
# ------------------------------------------------------------------
async def async_step_user(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
self._data.update(user_input)
if user_input[CONF_AUTH_METHOD] == AUTH_METHOD_PASSWORD:
return await self.async_step_credentials_password()
return await self.async_step_credentials_token()
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOMESERVER): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.URL)
),
vol.Required(CONF_AUTH_METHOD, default=AUTH_METHOD_PASSWORD): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(
value=AUTH_METHOD_PASSWORD,
label="Benutzername + Passwort",
),
selector.SelectOptionDict(
value=AUTH_METHOD_TOKEN,
label="Access Token",
),
],
mode=selector.SelectSelectorMode.LIST,
)
),
}
),
errors=errors,
)
# ------------------------------------------------------------------
# Step 2a: password login
# ------------------------------------------------------------------
async def async_step_credentials_password(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
store_path = self.hass.config.path(f".storage/{DOMAIN}")
client = MatrixClient(
homeserver=self._data[CONF_HOMESERVER],
user_id=user_input[CONF_USERNAME],
store_path=store_path,
)
try:
await client.async_setup()
token, device_id = await client.async_login_password(
user_input[CONF_PASSWORD],
user_input.get(CONF_DEVICE_NAME, DEFAULT_DEVICE_NAME),
)
self._available_rooms = await client.async_get_joined_rooms()
except MatrixClientError as err:
_LOGGER.error("Login fehlgeschlagen: %s", err)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unerwarteter Fehler beim Login")
errors["base"] = "unknown"
else:
self._data.update(
{
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_ACCESS_TOKEN: token,
CONF_DEVICE_ID: device_id,
CONF_DEVICE_NAME: user_input.get(CONF_DEVICE_NAME, DEFAULT_DEVICE_NAME),
}
)
return await self.async_step_rooms()
finally:
await client.async_close()
return self.async_show_form(
step_id="credentials_password",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT,
autocomplete="username",
)
),
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
vol.Optional(CONF_DEVICE_NAME, default=DEFAULT_DEVICE_NAME): selector.TextSelector(),
}
),
errors=errors,
)
# ------------------------------------------------------------------
# Step 2b: token login
# ------------------------------------------------------------------
async def async_step_credentials_token(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
store_path = self.hass.config.path(f".storage/{DOMAIN}")
client = MatrixClient(
homeserver=self._data[CONF_HOMESERVER],
user_id=user_input[CONF_USERNAME],
store_path=store_path,
)
try:
await client.async_setup()
# Fetch device_id from server using the provided token
_, device_id = await client.async_whoami_device_id(
user_input[CONF_ACCESS_TOKEN]
)
device_id = await client.async_restore_login(
access_token=user_input[CONF_ACCESS_TOKEN],
device_id=device_id,
)
self._available_rooms = await client.async_get_joined_rooms()
except MatrixClientError as err:
_LOGGER.error("Token-Login fehlgeschlagen: %s", err)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unerwarteter Fehler beim Token-Login")
errors["base"] = "unknown"
else:
self._data.update(
{
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_ACCESS_TOKEN: user_input[CONF_ACCESS_TOKEN],
CONF_DEVICE_ID: device_id,
}
)
return await self.async_step_rooms()
finally:
await client.async_close()
return self.async_show_form(
step_id="credentials_token",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Required(CONF_ACCESS_TOKEN): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
}
),
errors=errors,
description_placeholders={
"token_help": "Den Access Token findest du in deinem Matrix-Client unter Einstellungen → Sicherheit → Sitzungen."
},
)
# ------------------------------------------------------------------
# Step 3: room selection
# ------------------------------------------------------------------
async def async_step_rooms(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
selected = user_input.get(CONF_ROOMS, [])
self._data[CONF_ROOMS] = {
rid: self._available_rooms[rid]
for rid in selected
if rid in self._available_rooms
}
self._data[CONF_ENABLE_SYNC] = user_input.get(CONF_ENABLE_SYNC, False)
return self.async_create_entry(
title=self._data.get(CONF_USERNAME, self._data[CONF_HOMESERVER]),
data=self._data,
)
room_options = [
selector.SelectOptionDict(value=rid, label=f"{name} ({rid})")
for rid, name in self._available_rooms.items()
]
return self.async_show_form(
step_id="rooms",
data_schema=vol.Schema(
{
vol.Required(CONF_ROOMS): selector.SelectSelector(
selector.SelectSelectorConfig(
options=room_options,
multiple=True,
mode=selector.SelectSelectorMode.LIST,
)
),
vol.Optional(CONF_ENABLE_SYNC, default=False): selector.BooleanSelector(),
}
),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
return MatrixMessengerOptionsFlow(config_entry)
# ----------------------------------------------------------------------
# Options flow reconfigure rooms and sync after initial setup
# ----------------------------------------------------------------------
class MatrixMessengerOptionsFlow(config_entries.OptionsFlow):
"""Allow re-selection of rooms and sync toggle without re-authentication."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self._entry = config_entry
self._available_rooms: dict[str, str] = {}
async def async_step_init(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
selected = user_input.get(CONF_ROOMS, [])
new_rooms = {
rid: self._available_rooms.get(rid, rid)
for rid in selected
}
return self.async_create_entry(
title="",
data={
CONF_ROOMS: new_rooms,
CONF_ENABLE_SYNC: user_input.get(CONF_ENABLE_SYNC, False),
},
)
# Try to load fresh room list from the running client
domain_data = self.hass.data.get(DOMAIN, {})
entry_data = domain_data.get(self._entry.entry_id)
if entry_data is not None:
try:
self._available_rooms = await entry_data.client.async_get_joined_rooms()
except Exception:
_LOGGER.warning("Konnte Räume nicht neu laden, zeige gespeicherte Auswahl.")
if not self._available_rooms:
self._available_rooms = _effective_rooms(self._entry)
current_rooms = list(_effective_rooms(self._entry).keys())
room_options = [
selector.SelectOptionDict(value=rid, label=f"{name} ({rid})")
for rid, name in self._available_rooms.items()
]
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(CONF_ROOMS, default=current_rooms): selector.SelectSelector(
selector.SelectSelectorConfig(
options=room_options,
multiple=True,
mode=selector.SelectSelectorMode.LIST,
)
),
vol.Optional(
CONF_ENABLE_SYNC,
default=_effective_sync(self._entry),
): selector.BooleanSelector(),
}
),
errors=errors,
)
# ------------------------------------------------------------------
# Helpers shared between flows
# ------------------------------------------------------------------
def _effective_rooms(entry: config_entries.ConfigEntry) -> dict[str, str]:
return entry.options.get(CONF_ROOMS, entry.data.get(CONF_ROOMS, {}))
def _effective_sync(entry: config_entries.ConfigEntry) -> bool:
return entry.options.get(CONF_ENABLE_SYNC, entry.data.get(CONF_ENABLE_SYNC, False))