Files
Matrix-Server/custom_components/matrix_messenger/config_flow.py
T
Ceddy 39dbbe5eee v1.2.0: HACS-Kompatibilität, App-Icon, bereinigte Konfiguration
- manifest.json: codeowners, documentation, issue_tracker, mdi:matrix icon
- hacs.json: homeassistant-Feld entfernt (gehört in manifest.json)
- brand/: icon.png, icon@2x.png, dark_icon.png, dark_icon@2x.png hinzugefügt
- icon/icon.svg: Matrix-Chat SVG-Icon hinzugefügt
- generate_icons.py: PNG-Generator für brand/-Verzeichnis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 00:30:56 +02:00

352 lines
14 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 re
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_ACCOUNT_LABEL,
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__)
_LABEL_RE = re.compile(r'^[a-z0-9][a-z0-9_]{0,29}$')
class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Multi-step config flow: label + server → credentials → room selection."""
VERSION = 1
def __init__(self) -> None:
self._data: dict = {}
self._available_rooms: dict[str, str] = {}
# ------------------------------------------------------------------
# Step 1: account label + homeserver + auth method
# ------------------------------------------------------------------
async def async_step_user(self, user_input=None):
errors: dict[str, str] = {}
if user_input is not None:
label = user_input.get(CONF_ACCOUNT_LABEL, "").strip().lower()
if not _LABEL_RE.match(label):
errors[CONF_ACCOUNT_LABEL] = "invalid_label"
else:
self._data.update(user_input)
self._data[CONF_ACCOUNT_LABEL] = label
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_ACCOUNT_LABEL): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
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()
_, 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 (optional bridge-only accounts need no rooms)
# ------------------------------------------------------------------
async def async_step_rooms(self, user_input=None):
if user_input is not None:
selected = user_input.get(CONF_ROOMS, [])
manual_raw = user_input.get("manual_room_ids", "").strip()
rooms: dict[str, str] = {
rid: self._available_rooms.get(rid, rid)
for rid in selected
if rid
}
for raw in manual_raw.replace(",", " ").split():
rid = raw.strip()
if rid:
rooms[rid] = rid
self._data[CONF_ROOMS] = rooms
self._data[CONF_ENABLE_SYNC] = user_input.get(CONF_ENABLE_SYNC, False)
label = self._data.get(CONF_ACCOUNT_LABEL) or self._data.get(CONF_USERNAME, self._data[CONF_HOMESERVER])
return self.async_create_entry(title=label, data=self._data)
has_rooms = bool(self._available_rooms)
schema_dict: dict = {}
if has_rooms:
room_options = [
selector.SelectOptionDict(value=rid, label=name)
for rid, name in self._available_rooms.items()
]
schema_dict[vol.Optional(CONF_ROOMS, default=[])] = selector.SelectSelector(
selector.SelectSelectorConfig(
options=room_options,
multiple=True,
mode=selector.SelectSelectorMode.DROPDOWN,
)
)
schema_dict[vol.Optional("manual_room_ids", default="")] = selector.TextSelector(
selector.TextSelectorConfig(multiline=False)
)
schema_dict[vol.Optional(CONF_ENABLE_SYNC, default=False)] = selector.BooleanSelector()
return self.async_show_form(
step_id="rooms",
data_schema=vol.Schema(schema_dict),
description_placeholders={
"hint": (
"Räume sind optional. Bridge-Konten (WhatsApp, Signal, Telegram) "
"benötigen keine Räume dort genügt der Service 'Direktnachricht senden'."
)
},
)
@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),
},
)
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=name)
for rid, name in self._available_rooms.items()
]
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Optional(CONF_ROOMS, default=current_rooms): selector.SelectSelector(
selector.SelectSelectorConfig(
options=room_options,
multiple=True,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
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))