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>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
Ermöglicht das Senden von Nachrichten an Matrix-Räume sowie das
|
Ermöglicht das Senden von Nachrichten an Matrix-Räume sowie das
|
||||||
Stellen von Fragen mit Antwortwartezeit (Text oder Emoji-Reaktion).
|
Stellen von Fragen mit Antwortwartezeit (Text oder Emoji-Reaktion).
|
||||||
|
Unterstützt mehrere Accounts (z. B. für mautrix-Bridges).
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ from homeassistant.core import HomeAssistant, ServiceCall
|
|||||||
from .config_flow import _effective_rooms, _effective_sync
|
from .config_flow import _effective_rooms, _effective_sync
|
||||||
from .const import (
|
from .const import (
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_ACCOUNT_LABEL,
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_HOMESERVER,
|
CONF_HOMESERVER,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
@@ -78,7 +80,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
device_id=entry.data.get(CONF_DEVICE_ID, ""),
|
device_id=entry.data.get(CONF_DEVICE_ID, ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch room display names via direct state API (independent of sync state)
|
|
||||||
stored_rooms = _effective_rooms(entry)
|
stored_rooms = _effective_rooms(entry)
|
||||||
try:
|
try:
|
||||||
display_names = await client.async_get_room_names(list(stored_rooms.keys()))
|
display_names = await client.async_get_room_names(list(stored_rooms.keys()))
|
||||||
@@ -117,20 +118,28 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
_cancel_sync(data)
|
_cancel_sync(data)
|
||||||
await data.client.async_close()
|
await data.client.async_close()
|
||||||
|
|
||||||
all_services = ["send_message", "ask_question", "send_to_user"]
|
label = _effective_label(entry)
|
||||||
|
all_services = [
|
||||||
|
f"{label}_send_message",
|
||||||
|
f"{label}_ask_question",
|
||||||
|
f"{label}_send_to_user",
|
||||||
|
]
|
||||||
if data:
|
if data:
|
||||||
all_services.extend(data.room_service_names)
|
all_services.extend(data.room_service_names)
|
||||||
for service_name in all_services:
|
for service_name in all_services:
|
||||||
if hass.services.has_service(DOMAIN, service_name):
|
if hass.services.has_service(DOMAIN, service_name):
|
||||||
hass.services.async_remove(DOMAIN, service_name)
|
hass.services.async_remove(DOMAIN, service_name)
|
||||||
|
|
||||||
# Remove injected descriptions from cache so stale entries don't linger
|
|
||||||
try:
|
try:
|
||||||
from homeassistant.loader import SERVICE_DESCRIPTION_CACHE as _cache_key
|
from homeassistant.loader import SERVICE_DESCRIPTION_CACHE as _cache_key
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_cache_key = "service_description_cache"
|
_cache_key = "service_description_cache"
|
||||||
cache = hass.data.get(_cache_key, {})
|
cache = hass.data.get(_cache_key, {})
|
||||||
for svc in ["send_message", "ask_question"] + (data.room_service_names if data else []):
|
for svc in [
|
||||||
|
f"{label}_send_message",
|
||||||
|
f"{label}_ask_question",
|
||||||
|
f"{label}_send_to_user",
|
||||||
|
] + (data.room_service_names if data else []):
|
||||||
cache.pop((DOMAIN, svc), None)
|
cache.pop((DOMAIN, svc), None)
|
||||||
|
|
||||||
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
@@ -142,6 +151,27 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non
|
|||||||
await hass.config_entries.async_reload(entry.entry_id)
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _effective_label(entry: ConfigEntry) -> str:
|
||||||
|
"""Return the account label; fall back to a slug derived from the username."""
|
||||||
|
label = entry.data.get(CONF_ACCOUNT_LABEL, "").strip()
|
||||||
|
if not label:
|
||||||
|
username = entry.data.get(CONF_USERNAME, "")
|
||||||
|
label = re.sub(r"[^a-z0-9]+", "_", username.split(":")[0].lstrip("@").lower()).strip("_")
|
||||||
|
return label or "matrix"
|
||||||
|
|
||||||
|
|
||||||
|
def _room_slug(name: str) -> str:
|
||||||
|
"""Convert a room display name to a valid HA service name fragment."""
|
||||||
|
slug = name.lower()
|
||||||
|
slug = re.sub(r"[^a-z0-9]+", "_", slug).strip("_")
|
||||||
|
return slug or "room"
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Service registration
|
# Service registration
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -149,15 +179,16 @@ async def _async_options_updated(hass: HomeAssistant, entry: ConfigEntry) -> Non
|
|||||||
|
|
||||||
def _inject_service_descriptions(
|
def _inject_service_descriptions(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
label: str,
|
||||||
rooms: dict[str, str],
|
rooms: dict[str, str],
|
||||||
room_service_names: list[str],
|
room_service_names: list[str],
|
||||||
display_names: dict[str, str] | None = None,
|
display_names: dict[str, str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Inject dynamic service descriptions so HA shows friendly dropdowns and text fields."""
|
"""Inject dynamic service descriptions into HA's cache."""
|
||||||
try:
|
try:
|
||||||
from homeassistant.loader import SERVICE_DESCRIPTION_CACHE as _cache_key
|
from homeassistant.loader import SERVICE_DESCRIPTION_CACHE as _cache_key
|
||||||
except ImportError:
|
except ImportError:
|
||||||
_cache_key = "service_description_cache" # legacy fallback
|
_cache_key = "service_description_cache"
|
||||||
|
|
||||||
cache: dict = hass.data.setdefault(_cache_key, {})
|
cache: dict = hass.data.setdefault(_cache_key, {})
|
||||||
labels = display_names or {}
|
labels = display_names or {}
|
||||||
@@ -168,6 +199,33 @@ def _inject_service_descriptions(
|
|||||||
"required": True,
|
"required": True,
|
||||||
"selector": {"text": {"multiline": True}},
|
"selector": {"text": {"multiline": True}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# send_to_user – immer verfügbar (Bridge-Nutzung)
|
||||||
|
cache[(DOMAIN, f"{label}_send_to_user")] = {
|
||||||
|
"name": f"Matrix DM senden [{label}]",
|
||||||
|
"description": (
|
||||||
|
"Sendet eine Direktnachricht oder Bridge-Nachricht (WhatsApp, Signal, Telegram) "
|
||||||
|
"an eine Matrix-User-ID."
|
||||||
|
),
|
||||||
|
"fields": {
|
||||||
|
"user_id": {
|
||||||
|
"name": "Ziel-User-ID",
|
||||||
|
"description": (
|
||||||
|
"Matrix-User-ID des Empfängers, z. B. "
|
||||||
|
"@whatsapp_4917612345678:server.de oder "
|
||||||
|
"@signal_+4917612345678:server.de oder "
|
||||||
|
"@telegram_123456789:server.de"
|
||||||
|
),
|
||||||
|
"required": True,
|
||||||
|
"selector": {"text": {}},
|
||||||
|
},
|
||||||
|
"message": msg_field,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if not rooms:
|
||||||
|
return
|
||||||
|
|
||||||
room_options = [
|
room_options = [
|
||||||
{"value": rid, "label": labels.get(rid) or stored or rid}
|
{"value": rid, "label": labels.get(rid) or stored or rid}
|
||||||
for rid, stored in rooms.items()
|
for rid, stored in rooms.items()
|
||||||
@@ -179,13 +237,13 @@ def _inject_service_descriptions(
|
|||||||
"selector": {"select": {"options": room_options, "mode": "dropdown"}},
|
"selector": {"select": {"options": room_options, "mode": "dropdown"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
cache[(DOMAIN, "send_message")] = {
|
cache[(DOMAIN, f"{label}_send_message")] = {
|
||||||
"name": "Matrix-Nachricht senden",
|
"name": f"Matrix-Nachricht senden [{label}]",
|
||||||
"description": "Sendet eine Textnachricht an einen konfigurierten Matrix-Raum.",
|
"description": "Sendet eine Textnachricht an einen konfigurierten Matrix-Raum.",
|
||||||
"fields": {"room_id": room_field, "message": msg_field},
|
"fields": {"room_id": room_field, "message": msg_field},
|
||||||
}
|
}
|
||||||
cache[(DOMAIN, "ask_question")] = {
|
cache[(DOMAIN, f"{label}_ask_question")] = {
|
||||||
"name": "Frage in Matrix-Raum stellen",
|
"name": f"Frage in Matrix-Raum stellen [{label}]",
|
||||||
"description": (
|
"description": (
|
||||||
"Sendet eine Frage und wartet auf Antwort (Text oder Emoji-Reaktion). "
|
"Sendet eine Frage und wartet auf Antwort (Text oder Emoji-Reaktion). "
|
||||||
"Löst das Event 'matrix_messenger_response' aus."
|
"Löst das Event 'matrix_messenger_response' aus."
|
||||||
@@ -217,67 +275,28 @@ def _inject_service_descriptions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for service_name in room_service_names:
|
for service_name in room_service_names:
|
||||||
slug = service_name[len("send_to_"):]
|
prefix = f"{label}_send_to_"
|
||||||
|
slug = service_name[len(prefix):]
|
||||||
room_id = next(
|
room_id = next(
|
||||||
(rid for rid, name in rooms.items() if _room_slug(name) == slug),
|
(rid for rid, name in rooms.items() if _room_slug(name) == slug),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
label = (labels.get(room_id) or rooms.get(room_id) or slug) if room_id else slug
|
room_label = (labels.get(room_id) or rooms.get(room_id) or slug) if room_id else slug
|
||||||
cache[(DOMAIN, service_name)] = {
|
cache[(DOMAIN, service_name)] = {
|
||||||
"name": f"Matrix → {label}",
|
"name": f"Matrix → {room_label} [{label}]",
|
||||||
"description": f'Sendet eine Nachricht an den Matrix-Raum "{label}".',
|
"description": f'Sendet eine Nachricht an den Matrix-Raum "{room_label}" ({label}).',
|
||||||
"fields": {"message": msg_field},
|
"fields": {"message": msg_field},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _room_slug(name: str) -> str:
|
|
||||||
"""Convert a room display name to a valid HA service name fragment."""
|
|
||||||
slug = name.lower()
|
|
||||||
slug = re.sub(r"[^a-z0-9]+", "_", slug).strip("_")
|
|
||||||
return slug or "room"
|
|
||||||
|
|
||||||
|
|
||||||
def _register_services(
|
def _register_services(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, data: MatrixEntryData
|
hass: HomeAssistant, entry: ConfigEntry, data: MatrixEntryData
|
||||||
) -> None:
|
) -> None:
|
||||||
|
label = _effective_label(entry)
|
||||||
rooms = _effective_rooms(entry)
|
rooms = _effective_rooms(entry)
|
||||||
room_ids = list(rooms.keys())
|
room_ids = list(rooms.keys())
|
||||||
room_validator = vol.In(room_ids) if room_ids else str
|
|
||||||
|
|
||||||
async def handle_send_message(call: ServiceCall) -> None:
|
|
||||||
room_id: str = call.data["room_id"]
|
|
||||||
message: str = call.data["message"]
|
|
||||||
success = await data.client.async_send_message(room_id, message)
|
|
||||||
if not success:
|
|
||||||
_LOGGER.error("Nachricht an %s konnte nicht gesendet werden", room_id)
|
|
||||||
|
|
||||||
async def handle_ask_question(call: ServiceCall) -> None:
|
|
||||||
room_id: str = call.data["room_id"]
|
|
||||||
question: str = call.data["question"]
|
|
||||||
options: list[str] = call.data.get("options", [])
|
|
||||||
timeout: int = call.data.get("timeout", DEFAULT_QUESTION_TIMEOUT)
|
|
||||||
|
|
||||||
text = question
|
|
||||||
if options:
|
|
||||||
text = f"{question}\n\nMögliche Antworten: {' / '.join(options)}"
|
|
||||||
|
|
||||||
await data.client.async_send_message(room_id, text)
|
|
||||||
|
|
||||||
qid = str(uuid.uuid4())
|
|
||||||
data.pending_questions[qid] = PendingQuestion(
|
|
||||||
question_id=qid,
|
|
||||||
room_id=room_id,
|
|
||||||
options=options,
|
|
||||||
expires_at=time.monotonic() + timeout,
|
|
||||||
)
|
|
||||||
_LOGGER.debug("Frage %s wartet auf Antwort in Raum %s", qid, room_id)
|
|
||||||
|
|
||||||
if data.sync_task is None or data.sync_task.done():
|
|
||||||
data.sync_task = hass.async_create_background_task(
|
|
||||||
_sync_loop(hass, entry, data, stop_when_idle=True),
|
|
||||||
name=f"{DOMAIN}_sync_{entry.entry_id}",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# send_to_user – immer registrieren (Bridge + native Matrix DM)
|
||||||
async def handle_send_to_user(call: ServiceCall) -> None:
|
async def handle_send_to_user(call: ServiceCall) -> None:
|
||||||
user_id: str = call.data["user_id"]
|
user_id: str = call.data["user_id"]
|
||||||
message: str = call.data["message"]
|
message: str = call.data["message"]
|
||||||
@@ -287,35 +306,7 @@ def _register_services(
|
|||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
"send_message",
|
f"{label}_send_to_user",
|
||||||
handle_send_message,
|
|
||||||
schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("room_id"): room_validator,
|
|
||||||
vol.Required("message"): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"ask_question",
|
|
||||||
handle_ask_question,
|
|
||||||
schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required("room_id"): room_validator,
|
|
||||||
vol.Required("question"): str,
|
|
||||||
vol.Optional("options", default=[]): [str],
|
|
||||||
vol.Optional("timeout", default=DEFAULT_QUESTION_TIMEOUT): vol.All(
|
|
||||||
int, vol.Range(min=60, max=7200)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"send_to_user",
|
|
||||||
handle_send_to_user,
|
handle_send_to_user,
|
||||||
schema=vol.Schema(
|
schema=vol.Schema(
|
||||||
{
|
{
|
||||||
@@ -325,37 +316,102 @@ def _register_services(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Per-room convenience services: matrix_messenger.send_to_<slug>
|
# Raum-basierte Services nur wenn Räume konfiguriert sind
|
||||||
# (registered first so room_service_names is populated before injection)
|
if room_ids:
|
||||||
used_slugs: set[str] = set()
|
room_validator = vol.In(room_ids)
|
||||||
for room_id, room_name in rooms.items():
|
|
||||||
base = _room_slug(room_name)
|
|
||||||
slug = base
|
|
||||||
counter = 2
|
|
||||||
while slug in used_slugs:
|
|
||||||
slug = f"{base}_{counter}"
|
|
||||||
counter += 1
|
|
||||||
used_slugs.add(slug)
|
|
||||||
|
|
||||||
service_name = f"send_to_{slug}"
|
async def handle_send_message(call: ServiceCall) -> None:
|
||||||
data.room_service_names.append(service_name)
|
room_id: str = call.data["room_id"]
|
||||||
|
message: str = call.data["message"]
|
||||||
|
success = await data.client.async_send_message(room_id, message)
|
||||||
|
if not success:
|
||||||
|
_LOGGER.error("Nachricht an %s konnte nicht gesendet werden", room_id)
|
||||||
|
|
||||||
def _make_handler(rid: str, rname: str):
|
async def handle_ask_question(call: ServiceCall) -> None:
|
||||||
async def handler(call: ServiceCall) -> None:
|
room_id: str = call.data["room_id"]
|
||||||
msg: str = call.data["message"]
|
question: str = call.data["question"]
|
||||||
success = await data.client.async_send_message(rid, msg)
|
options: list[str] = call.data.get("options", [])
|
||||||
if not success:
|
timeout: int = call.data.get("timeout", DEFAULT_QUESTION_TIMEOUT)
|
||||||
_LOGGER.error("Nachricht an %s (%s) konnte nicht gesendet werden", rname, rid)
|
|
||||||
return handler
|
text = question
|
||||||
|
if options:
|
||||||
|
text = f"{question}\n\nMögliche Antworten: {' / '.join(options)}"
|
||||||
|
|
||||||
|
await data.client.async_send_message(room_id, text)
|
||||||
|
|
||||||
|
qid = str(uuid.uuid4())
|
||||||
|
data.pending_questions[qid] = PendingQuestion(
|
||||||
|
question_id=qid,
|
||||||
|
room_id=room_id,
|
||||||
|
options=options,
|
||||||
|
expires_at=time.monotonic() + timeout,
|
||||||
|
)
|
||||||
|
_LOGGER.debug("Frage %s wartet auf Antwort in Raum %s", qid, room_id)
|
||||||
|
|
||||||
|
if data.sync_task is None or data.sync_task.done():
|
||||||
|
data.sync_task = hass.async_create_background_task(
|
||||||
|
_sync_loop(hass, entry, data, stop_when_idle=True),
|
||||||
|
name=f"{DOMAIN}_sync_{entry.entry_id}",
|
||||||
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
service_name,
|
f"{label}_send_message",
|
||||||
_make_handler(room_id, room_name),
|
handle_send_message,
|
||||||
schema=vol.Schema({vol.Required("message"): str}),
|
schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("room_id"): room_validator,
|
||||||
|
vol.Required("message"): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
_inject_service_descriptions(hass, rooms, data.room_service_names, data.display_names)
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
f"{label}_ask_question",
|
||||||
|
handle_ask_question,
|
||||||
|
schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required("room_id"): room_validator,
|
||||||
|
vol.Required("question"): str,
|
||||||
|
vol.Optional("options", default=[]): [str],
|
||||||
|
vol.Optional("timeout", default=DEFAULT_QUESTION_TIMEOUT): vol.All(
|
||||||
|
int, vol.Range(min=60, max=7200)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Per-room convenience services: matrix_messenger.{label}_send_to_{slug}
|
||||||
|
used_slugs: set[str] = set()
|
||||||
|
for room_id, room_name in rooms.items():
|
||||||
|
base = _room_slug(room_name)
|
||||||
|
slug = base
|
||||||
|
counter = 2
|
||||||
|
while slug in used_slugs:
|
||||||
|
slug = f"{base}_{counter}"
|
||||||
|
counter += 1
|
||||||
|
used_slugs.add(slug)
|
||||||
|
|
||||||
|
service_name = f"{label}_send_to_{slug}"
|
||||||
|
data.room_service_names.append(service_name)
|
||||||
|
|
||||||
|
def _make_handler(rid: str, rname: str):
|
||||||
|
async def handler(call: ServiceCall) -> None:
|
||||||
|
msg: str = call.data["message"]
|
||||||
|
success = await data.client.async_send_message(rid, msg)
|
||||||
|
if not success:
|
||||||
|
_LOGGER.error("Nachricht an %s (%s) konnte nicht gesendet werden", rname, rid)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
service_name,
|
||||||
|
_make_handler(room_id, room_name),
|
||||||
|
schema=vol.Schema({vol.Required("message"): str}),
|
||||||
|
)
|
||||||
|
|
||||||
|
_inject_service_descriptions(hass, label, rooms, data.room_service_names, data.display_names)
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -369,16 +425,11 @@ async def _sync_loop(
|
|||||||
data: MatrixEntryData,
|
data: MatrixEntryData,
|
||||||
stop_when_idle: bool,
|
stop_when_idle: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Poll Matrix every DEFAULT_SYNC_INTERVAL seconds.
|
"""Poll Matrix every DEFAULT_SYNC_INTERVAL seconds."""
|
||||||
|
|
||||||
When stop_when_idle=True, the loop exits automatically once all
|
|
||||||
pending questions have been answered or expired.
|
|
||||||
"""
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
await data.client.async_sync_once(timeout_ms=5000)
|
await data.client.async_sync_once(timeout_ms=5000)
|
||||||
|
|
||||||
# Expire old questions
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
expired = [qid for qid, q in data.pending_questions.items() if now > q.expires_at]
|
expired = [qid for qid, q in data.pending_questions.items() if now > q.expires_at]
|
||||||
for qid in expired:
|
for qid in expired:
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@@ -12,6 +13,7 @@ from .const import (
|
|||||||
AUTH_METHOD_PASSWORD,
|
AUTH_METHOD_PASSWORD,
|
||||||
AUTH_METHOD_TOKEN,
|
AUTH_METHOD_TOKEN,
|
||||||
CONF_ACCESS_TOKEN,
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_ACCOUNT_LABEL,
|
||||||
CONF_AUTH_METHOD,
|
CONF_AUTH_METHOD,
|
||||||
CONF_DEVICE_ID,
|
CONF_DEVICE_ID,
|
||||||
CONF_DEVICE_NAME,
|
CONF_DEVICE_NAME,
|
||||||
@@ -27,9 +29,11 @@ from .matrix_client import MatrixClient, MatrixClientError
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_LABEL_RE = re.compile(r'^[a-z0-9][a-z0-9_]{0,29}$')
|
||||||
|
|
||||||
|
|
||||||
class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Multi-step config flow: server → credentials → room selection."""
|
"""Multi-step config flow: label + server → credentials → room selection."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
@@ -38,21 +42,29 @@ class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
self._available_rooms: dict[str, str] = {}
|
self._available_rooms: dict[str, str] = {}
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Step 1: homeserver + auth method
|
# Step 1: account label + homeserver + auth method
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._data.update(user_input)
|
label = user_input.get(CONF_ACCOUNT_LABEL, "").strip().lower()
|
||||||
if user_input[CONF_AUTH_METHOD] == AUTH_METHOD_PASSWORD:
|
if not _LABEL_RE.match(label):
|
||||||
return await self.async_step_credentials_password()
|
errors[CONF_ACCOUNT_LABEL] = "invalid_label"
|
||||||
return await self.async_step_credentials_token()
|
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(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Required(CONF_ACCOUNT_LABEL): selector.TextSelector(
|
||||||
|
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
|
||||||
|
),
|
||||||
vol.Required(CONF_HOMESERVER): selector.TextSelector(
|
vol.Required(CONF_HOMESERVER): selector.TextSelector(
|
||||||
selector.TextSelectorConfig(type=selector.TextSelectorType.URL)
|
selector.TextSelectorConfig(type=selector.TextSelectorType.URL)
|
||||||
),
|
),
|
||||||
@@ -152,7 +164,6 @@ class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await client.async_setup()
|
await client.async_setup()
|
||||||
# Fetch device_id from server using the provided token
|
|
||||||
_, device_id = await client.async_whoami_device_id(
|
_, device_id = await client.async_whoami_device_id(
|
||||||
user_input[CONF_ACCESS_TOKEN]
|
user_input[CONF_ACCESS_TOKEN]
|
||||||
)
|
)
|
||||||
@@ -198,13 +209,10 @@ class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Step 3: room selection
|
# Step 3: room selection (optional – bridge-only accounts need no rooms)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
async def async_step_rooms(self, user_input=None):
|
async def async_step_rooms(self, user_input=None):
|
||||||
errors: dict[str, str] = {}
|
|
||||||
has_rooms = bool(self._available_rooms)
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
selected = user_input.get(CONF_ROOMS, [])
|
selected = user_input.get(CONF_ROOMS, [])
|
||||||
manual_raw = user_input.get("manual_room_ids", "").strip()
|
manual_raw = user_input.get("manual_room_ids", "").strip()
|
||||||
@@ -219,16 +227,12 @@ class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if rid:
|
if rid:
|
||||||
rooms[rid] = rid
|
rooms[rid] = rid
|
||||||
|
|
||||||
if not rooms:
|
self._data[CONF_ROOMS] = rooms
|
||||||
errors["base"] = "no_rooms"
|
self._data[CONF_ENABLE_SYNC] = user_input.get(CONF_ENABLE_SYNC, False)
|
||||||
else:
|
label = self._data.get(CONF_ACCOUNT_LABEL) or self._data.get(CONF_USERNAME, self._data[CONF_HOMESERVER])
|
||||||
self._data[CONF_ROOMS] = rooms
|
return self.async_create_entry(title=label, data=self._data)
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
has_rooms = bool(self._available_rooms)
|
||||||
schema_dict: dict = {}
|
schema_dict: dict = {}
|
||||||
|
|
||||||
if has_rooms:
|
if has_rooms:
|
||||||
@@ -236,35 +240,28 @@ class MatrixMessengerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
selector.SelectOptionDict(value=rid, label=name)
|
selector.SelectOptionDict(value=rid, label=name)
|
||||||
for rid, name in self._available_rooms.items()
|
for rid, name in self._available_rooms.items()
|
||||||
]
|
]
|
||||||
schema_dict[vol.Required(CONF_ROOMS)] = selector.SelectSelector(
|
schema_dict[vol.Optional(CONF_ROOMS, default=[])] = selector.SelectSelector(
|
||||||
selector.SelectSelectorConfig(
|
selector.SelectSelectorConfig(
|
||||||
options=room_options,
|
options=room_options,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
schema_dict[vol.Optional("manual_room_ids", default="")] = selector.TextSelector(
|
|
||||||
selector.TextSelectorConfig(multiline=False)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
schema_dict[vol.Required("manual_room_ids")] = selector.TextSelector(
|
|
||||||
selector.TextSelectorConfig(multiline=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
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()
|
schema_dict[vol.Optional(CONF_ENABLE_SYNC, default=False)] = selector.BooleanSelector()
|
||||||
|
|
||||||
placeholders = {}
|
|
||||||
if not has_rooms:
|
|
||||||
placeholders["hint"] = (
|
|
||||||
"Keine Räume gefunden. Raum-IDs eingeben (z.B. !abc123:chat.example.com), "
|
|
||||||
"mehrere mit Komma oder Leerzeichen trennen."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="rooms",
|
step_id="rooms",
|
||||||
data_schema=vol.Schema(schema_dict),
|
data_schema=vol.Schema(schema_dict),
|
||||||
description_placeholders=placeholders if placeholders else None,
|
description_placeholders={
|
||||||
errors=errors,
|
"hint": (
|
||||||
|
"Räume sind optional. Bridge-Konten (WhatsApp, Signal, Telegram) "
|
||||||
|
"benötigen keine Räume – dort genügt der Service 'Direktnachricht senden'."
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -302,7 +299,6 @@ class MatrixMessengerOptionsFlow(config_entries.OptionsFlow):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Try to load fresh room list from the running client
|
|
||||||
domain_data = self.hass.data.get(DOMAIN, {})
|
domain_data = self.hass.data.get(DOMAIN, {})
|
||||||
entry_data = domain_data.get(self._entry.entry_id)
|
entry_data = domain_data.get(self._entry.entry_id)
|
||||||
if entry_data is not None:
|
if entry_data is not None:
|
||||||
@@ -325,7 +321,7 @@ class MatrixMessengerOptionsFlow(config_entries.OptionsFlow):
|
|||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_ROOMS, default=current_rooms): selector.SelectSelector(
|
vol.Optional(CONF_ROOMS, default=current_rooms): selector.SelectSelector(
|
||||||
selector.SelectSelectorConfig(
|
selector.SelectSelectorConfig(
|
||||||
options=room_options,
|
options=room_options,
|
||||||
multiple=True,
|
multiple=True,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ CONF_DEVICE_ID = "device_id"
|
|||||||
CONF_DEVICE_NAME = "device_name"
|
CONF_DEVICE_NAME = "device_name"
|
||||||
CONF_ROOMS = "rooms"
|
CONF_ROOMS = "rooms"
|
||||||
CONF_ENABLE_SYNC = "enable_sync"
|
CONF_ENABLE_SYNC = "enable_sync"
|
||||||
|
CONF_ACCOUNT_LABEL = "account_label"
|
||||||
|
|
||||||
AUTH_METHOD_PASSWORD = "password"
|
AUTH_METHOD_PASSWORD = "password"
|
||||||
AUTH_METHOD_TOKEN = "token"
|
AUTH_METHOD_TOKEN = "token"
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Generate icon PNGs for the matrix_messenger integration.
|
||||||
|
Run once: python generate_icons.py
|
||||||
|
Requires: pip install Pillow
|
||||||
|
Output: icon.png, icon@2x.png, dark_icon.png, dark_icon@2x.png
|
||||||
|
"""
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
except ImportError:
|
||||||
|
raise SystemExit("Pillow not found. Install it with: pip install Pillow")
|
||||||
|
|
||||||
|
|
||||||
|
NAVY = (26, 26, 46, 255) # #1A1A2E (background)
|
||||||
|
PURPLE = (91, 45, 142, 255) # #5B2D8E (speech bubble)
|
||||||
|
TEAL = ( 3, 218, 198, 255) # #03DAC6 (badge)
|
||||||
|
WHITE = (255, 255, 255, 255)
|
||||||
|
TRANSPARENT = (0, 0, 0, 0)
|
||||||
|
|
||||||
|
DARK_BG = (18, 18, 18, 255) # near-black for dark-mode variant
|
||||||
|
|
||||||
|
|
||||||
|
def draw_icon(size: int, dark: bool) -> Image.Image:
|
||||||
|
s = size / 256
|
||||||
|
|
||||||
|
img = Image.new("RGBA", (size, size), TRANSPARENT)
|
||||||
|
d = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
bg = DARK_BG if dark else NAVY
|
||||||
|
|
||||||
|
# ── Outer rounded background ────────────────────────────────────────────
|
||||||
|
d.rounded_rectangle([0, 0, size - 1, size - 1], radius=int(48 * s), fill=bg)
|
||||||
|
|
||||||
|
# ── Speech bubble ───────────────────────────────────────────────────────
|
||||||
|
# Body (rounded rect, top + sides)
|
||||||
|
bx0, by0 = int(36 * s), int(36 * s)
|
||||||
|
bx1, by1 = int(220 * s), int(168 * s)
|
||||||
|
d.rounded_rectangle([bx0, by0, bx1, by1], radius=int(16 * s), fill=PURPLE)
|
||||||
|
|
||||||
|
# Tail triangle pointing down-center
|
||||||
|
cx = int(128 * s)
|
||||||
|
ty = int(168 * s)
|
||||||
|
tw = int(28 * s)
|
||||||
|
th = int(34 * s)
|
||||||
|
d.polygon(
|
||||||
|
[(cx - tw, ty), (cx + tw, ty), (cx, ty + th)],
|
||||||
|
fill=PURPLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Left bracket [ ──────────────────────────────────────────────────────
|
||||||
|
lx = int(60 * s)
|
||||||
|
bw = int(11 * s)
|
||||||
|
bh = int(60 * s)
|
||||||
|
cap = int(26 * s)
|
||||||
|
cr = int(3 * s)
|
||||||
|
by_top = int(78 * s)
|
||||||
|
by_bot = int(127 * s)
|
||||||
|
|
||||||
|
d.rounded_rectangle([lx, by_top, lx + bw, by_top + bh], radius=cr, fill=WHITE) # vertical
|
||||||
|
d.rounded_rectangle([lx, by_top, lx + cap, by_top + bw], radius=cr, fill=WHITE) # top cap
|
||||||
|
d.rounded_rectangle([lx, by_bot, lx + cap, by_bot + bw], radius=cr, fill=WHITE) # bottom cap
|
||||||
|
|
||||||
|
# ── Right bracket ] ─────────────────────────────────────────────────────
|
||||||
|
rx = int(185 * s)
|
||||||
|
d.rounded_rectangle([rx, by_top, rx + bw, by_top + bh], radius=cr, fill=WHITE)
|
||||||
|
d.rounded_rectangle([rx - cap + bw, by_top, rx + bw, by_top + bw], radius=cr, fill=WHITE)
|
||||||
|
d.rounded_rectangle([rx - cap + bw, by_bot, rx + bw, by_bot + bw], radius=cr, fill=WHITE)
|
||||||
|
|
||||||
|
# ── M letter (polyline: left-up, peak, right-up, right-down) ────────────
|
||||||
|
sw = int(12 * s)
|
||||||
|
pts = [
|
||||||
|
(int(97 * s), int(132 * s)),
|
||||||
|
(int(97 * s), int(84 * s)),
|
||||||
|
(int(128 * s), int(114 * s)),
|
||||||
|
(int(159 * s), int(84 * s)),
|
||||||
|
(int(159 * s), int(132 * s)),
|
||||||
|
]
|
||||||
|
for i in range(len(pts) - 1):
|
||||||
|
x0, y0 = pts[i]
|
||||||
|
x1, y1 = pts[i + 1]
|
||||||
|
d.line([x0, y0, x1, y1], fill=WHITE, width=sw)
|
||||||
|
# round caps
|
||||||
|
for px, py in pts:
|
||||||
|
r = sw // 2
|
||||||
|
d.ellipse([px - r, py - r, px + r, py + r], fill=WHITE)
|
||||||
|
|
||||||
|
# ── Teal badge with three dots ───────────────────────────────────────────
|
||||||
|
bc = int(196 * s)
|
||||||
|
br = int(36 * s)
|
||||||
|
d.ellipse([bc - br, bc - br, bc + br, bc + br], fill=TEAL)
|
||||||
|
|
||||||
|
dot_r = int(7 * s)
|
||||||
|
for dx in (-12, 0, 12):
|
||||||
|
cx2 = bc + int(dx * s)
|
||||||
|
d.ellipse([cx2 - dot_r, bc - dot_r, cx2 + dot_r, bc + dot_r], fill=bg)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
HERE = Path(__file__).parent / "brand"
|
||||||
|
HERE.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
variants = [
|
||||||
|
("icon.png", 256, False),
|
||||||
|
("icon@2x.png", 512, False),
|
||||||
|
("dark_icon.png", 256, True),
|
||||||
|
("dark_icon@2x.png", 512, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename, size, dark in variants:
|
||||||
|
path = HERE / filename
|
||||||
|
draw_icon(size, dark).save(path, "PNG")
|
||||||
|
print(f" created brand/{path.name} ({size}×{size})")
|
||||||
|
|
||||||
|
print("\nDone. Restart Home Assistant to pick up the new icons.")
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="256" height="256" rx="48" ry="48" fill="#1A1A2E"/>
|
||||||
|
|
||||||
|
<!-- Speech bubble body -->
|
||||||
|
<path d="M36,52 Q36,36 52,36 H204 Q220,36 220,52 V152 Q220,168 204,168 H152 L128,200 L104,168 H52 Q36,168 36,152 Z" fill="#5B2D8E"/>
|
||||||
|
|
||||||
|
<!-- Left bracket [ -->
|
||||||
|
<rect x="60" y="78" width="11" height="60" rx="3" fill="#FFFFFF"/>
|
||||||
|
<rect x="60" y="78" width="26" height="11" rx="3" fill="#FFFFFF"/>
|
||||||
|
<rect x="60" y="127" width="26" height="11" rx="3" fill="#FFFFFF"/>
|
||||||
|
|
||||||
|
<!-- Right bracket ] -->
|
||||||
|
<rect x="185" y="78" width="11" height="60" rx="3" fill="#FFFFFF"/>
|
||||||
|
<rect x="170" y="78" width="26" height="11" rx="3" fill="#FFFFFF"/>
|
||||||
|
<rect x="170" y="127" width="26" height="11" rx="3" fill="#FFFFFF"/>
|
||||||
|
|
||||||
|
<!-- M letter -->
|
||||||
|
<polyline
|
||||||
|
points="97,132 97,84 128,114 159,84 159,132"
|
||||||
|
fill="none"
|
||||||
|
stroke="#FFFFFF"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Signal dots (communication) bottom-right badge -->
|
||||||
|
<circle cx="196" cy="196" r="36" fill="#03DAC6"/>
|
||||||
|
<circle cx="184" cy="196" r="7" fill="#1A1A2E"/>
|
||||||
|
<circle cx="196" cy="196" r="7" fill="#1A1A2E"/>
|
||||||
|
<circle cx="208" cy="196" r="7" fill="#1A1A2E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"domain": "matrix_messenger",
|
"domain": "matrix_messenger",
|
||||||
"name": "Matrix Messenger",
|
"name": "Matrix Messenger",
|
||||||
"codeowners": [],
|
"version": "1.2.0",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "",
|
"documentation": "https://github.com/CeddysHomeAssistant/Matrix-Server",
|
||||||
"homeassistant": "2026.4.0",
|
"issue_tracker": "https://github.com/CeddysHomeAssistant/Matrix-Server/issues",
|
||||||
"iot_class": "cloud_push",
|
"codeowners": ["@CeddysHomeAssistant"],
|
||||||
"requirements": ["matrix-nio[e2e]>=0.21.0"],
|
"requirements": ["matrix-nio[e2e]>=0.21.0"],
|
||||||
"version": "1.1.0",
|
"dependencies": [],
|
||||||
"integration_type": "hub"
|
"iot_class": "cloud_push",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"homeassistant": "2026.4.0",
|
||||||
|
"icon": "mdi:matrix"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
"title": "Matrix-Server",
|
"title": "Matrix-Server",
|
||||||
"description": "Verbinde Home Assistant mit deinem Matrix-Homeserver.",
|
"description": "Verbinde Home Assistant mit deinem Matrix-Homeserver.",
|
||||||
"data": {
|
"data": {
|
||||||
|
"account_label": "Account-Label (z. B. 'marc' oder 'ha_chaos')",
|
||||||
"homeserver": "Homeserver-URL",
|
"homeserver": "Homeserver-URL",
|
||||||
"auth_method": "Anmeldeverfahren"
|
"auth_method": "Anmeldeverfahren"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"account_label": "Kurzname für diesen Account. Wird Präfix der Service-Namen (nur Kleinbuchstaben, Ziffern, Unterstriche)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"credentials_password": {
|
"credentials_password": {
|
||||||
@@ -27,15 +31,17 @@
|
|||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Matrix-Räume",
|
"title": "Matrix-Räume",
|
||||||
"description": "Wähle die Räume aus, in die Nachrichten gesendet werden sollen.",
|
"description": "Räume sind optional. Bridge-Konten (WhatsApp, Signal, Telegram) benötigen keine Räume – dort genügt der Service 'Direktnachricht senden'.",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Räume",
|
"rooms": "Räume (optional)",
|
||||||
|
"manual_room_ids": "Zusätzliche Raum-IDs (komma- oder leerzeichengetrennt)",
|
||||||
"enable_sync": "Hintergrund-Sync aktivieren (für Antwortempfang)"
|
"enable_sync": "Hintergrund-Sync aktivieren (für Antwortempfang)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Verbindung zum Matrix-Server fehlgeschlagen. Bitte URL und Zugangsdaten prüfen.",
|
"cannot_connect": "Verbindung zum Matrix-Server fehlgeschlagen. Bitte URL und Zugangsdaten prüfen.",
|
||||||
|
"invalid_label": "Das Label darf nur Kleinbuchstaben, Ziffern und Unterstriche enthalten und muss mit einem Buchstaben oder einer Ziffer beginnen (z. B. 'marc').",
|
||||||
"unknown": "Ein unerwarteter Fehler ist aufgetreten."
|
"unknown": "Ein unerwarteter Fehler ist aufgetreten."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
@@ -47,7 +53,7 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"title": "Matrix Messenger – Optionen",
|
"title": "Matrix Messenger – Optionen",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Räume",
|
"rooms": "Räume (optional)",
|
||||||
"enable_sync": "Hintergrund-Sync aktivieren"
|
"enable_sync": "Hintergrund-Sync aktivieren"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
"title": "Matrix-Server",
|
"title": "Matrix-Server",
|
||||||
"description": "Verbinde Home Assistant mit deinem Matrix-Homeserver.",
|
"description": "Verbinde Home Assistant mit deinem Matrix-Homeserver.",
|
||||||
"data": {
|
"data": {
|
||||||
|
"account_label": "Account-Label (z. B. 'marc' oder 'ha_chaos')",
|
||||||
"homeserver": "Homeserver-URL",
|
"homeserver": "Homeserver-URL",
|
||||||
"auth_method": "Anmeldeverfahren"
|
"auth_method": "Anmeldeverfahren"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"account_label": "Kurzname für diesen Account. Wird Präfix der Service-Namen (nur Kleinbuchstaben, Ziffern, Unterstriche)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"credentials_password": {
|
"credentials_password": {
|
||||||
@@ -26,16 +30,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Matrix-Räume auswählen",
|
"title": "Matrix-Räume",
|
||||||
"description": "Wähle die Räume aus, in die Nachrichten gesendet werden sollen.",
|
"description": "Räume sind optional. Bridge-Konten (WhatsApp, Signal, Telegram) benötigen keine Räume – dort genügt der Service 'Direktnachricht senden'.",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Räume",
|
"rooms": "Räume (optional)",
|
||||||
|
"manual_room_ids": "Zusätzliche Raum-IDs (komma- oder leerzeichengetrennt)",
|
||||||
"enable_sync": "Hintergrund-Sync aktivieren (für Antwortempfang)"
|
"enable_sync": "Hintergrund-Sync aktivieren (für Antwortempfang)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Verbindung zum Matrix-Server fehlgeschlagen. Bitte URL und Zugangsdaten prüfen.",
|
"cannot_connect": "Verbindung zum Matrix-Server fehlgeschlagen. Bitte URL und Zugangsdaten prüfen.",
|
||||||
|
"invalid_label": "Das Label darf nur Kleinbuchstaben, Ziffern und Unterstriche enthalten und muss mit einem Buchstaben oder einer Ziffer beginnen (z. B. 'marc').",
|
||||||
"unknown": "Ein unerwarteter Fehler ist aufgetreten."
|
"unknown": "Ein unerwarteter Fehler ist aufgetreten."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
@@ -47,7 +53,7 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"title": "Matrix Messenger – Optionen",
|
"title": "Matrix Messenger – Optionen",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Räume",
|
"rooms": "Räume (optional)",
|
||||||
"enable_sync": "Hintergrund-Sync aktivieren"
|
"enable_sync": "Hintergrund-Sync aktivieren"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,12 @@
|
|||||||
"title": "Matrix Server",
|
"title": "Matrix Server",
|
||||||
"description": "Connect Home Assistant to your Matrix homeserver.",
|
"description": "Connect Home Assistant to your Matrix homeserver.",
|
||||||
"data": {
|
"data": {
|
||||||
|
"account_label": "Account label (e.g. 'marc' or 'ha_chaos')",
|
||||||
"homeserver": "Homeserver URL",
|
"homeserver": "Homeserver URL",
|
||||||
"auth_method": "Authentication method"
|
"auth_method": "Authentication method"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"account_label": "Short name for this account. Used as prefix in service names (lowercase letters, digits, underscores only)."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"credentials_password": {
|
"credentials_password": {
|
||||||
@@ -26,16 +30,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rooms": {
|
"rooms": {
|
||||||
"title": "Select Matrix Rooms",
|
"title": "Matrix Rooms",
|
||||||
"description": "Choose the rooms you want to send messages to.",
|
"description": "Rooms are optional. Bridge accounts (WhatsApp, Signal, Telegram) do not need rooms – the 'Send direct message' service is sufficient.",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms (optional)",
|
||||||
|
"manual_room_ids": "Additional room IDs (comma- or space-separated)",
|
||||||
"enable_sync": "Enable background sync (required to receive replies)"
|
"enable_sync": "Enable background sync (required to receive replies)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "Could not connect to the Matrix server. Please check the URL and credentials.",
|
"cannot_connect": "Could not connect to the Matrix server. Please check the URL and credentials.",
|
||||||
|
"invalid_label": "The label may only contain lowercase letters, digits, and underscores, and must start with a letter or digit (e.g. 'marc').",
|
||||||
"unknown": "An unexpected error occurred."
|
"unknown": "An unexpected error occurred."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
@@ -47,7 +53,7 @@
|
|||||||
"init": {
|
"init": {
|
||||||
"title": "Matrix Messenger – Options",
|
"title": "Matrix Messenger – Options",
|
||||||
"data": {
|
"data": {
|
||||||
"rooms": "Rooms",
|
"rooms": "Rooms (optional)",
|
||||||
"enable_sync": "Enable background sync"
|
"enable_sync": "Enable background sync"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user