Nachrichten von FHEM zu Matrix Synapse senden

Begonnen von DecaTec, 03 Mai 2021, 12:42:21

Vorheriges Thema - Nächstes Thema

DecaTec

Hi,

vielleicht kennt jemand ja schon Matrix als Chat-System. Dies arbeitet dezentral mittels "Federation". D.h. jeder kann einen Server betreiben oder sich auf einem beliebigen Server anmelden und dann nicht nur mit Usern dieses Servers chatten, sondern auch mit Usern, die ihre Accounts auf anderen Servern haben.
Als kleiner Tipp: CoolTux hat hier schon mal einen Server aufgesetzt und hier tummeln sich bereits einige "FHEMler".

Ich betreibe nun seit einiger Zeit meinen eigenen Server und wollte nun mal eine Verbindung mit FHEM herstellen. Bisher habe ich dazu immer Telegram (mit dem dazugehörigen FHEM-Modul) genutzt, aber eigentlich ist Telegram ja aus Sicht von Privatsphäre und Datenschutz (meiner Meinung nach) eher bedenklich. Dazu wollte ich mal sehen, was mit Matrix so geht.

Ziel war es zunächst einmal nur, eine Nachricht aus FHEM heraus an einen Matrix-Raum zu schicken. Das ganze funktioniert mit der API von Matrix, die durch ein Stückchen Perl-Code angesprochen wird.

Voraussetzungen:
Zunächst braucht ihr natürlich einen Matrix-Account (entweder bei Matrix.org selbst, oder auf einem beliebigen anderen Server). Als Domain habe ich hier beispielhaft "matrix.meinedomain.de" verwendet.
Zusätzlich einen zweiten Account exklusiv für FHEM (optional, aber sicher empfehlenswert).
Die Authentifizierung geschieht hier mittels Token. Dieses Token (vom Matrix-FHEM-Account) bekommt ihr direkt im Client (Browser, Desktop, etc.) unter "Alle Einstellungen" > "Hilfe und Über" > ganz runter scrollen und hier auf "Zugangstoken" klicken. Diesen langen String schon mal kopieren. Siehe erstes Bild im Anhang.

Anschließend erstellt ihr euch einen Raum mit eurem persönlichen User und ladet den Matrix-FHEM-User in diesen Raum ein. Von diesem Raum braucht ihr noch die Raum-ID (Raumeinstellungen > "Erweitert" > "Interne Raum-ID". In diesem Beispiel "!abcxyz:matrix.meinedomain.de". Siehe zweites Bild im Anhang.

Bitte denkt daran, diesen Raum möglichst privat zu halten, also z.B. nicht öffentlich verfügbar zu machen (besser ist das).

Dann kommt ein bisschen Code (ich habe das in meiner 99_myUtils.pm als Funktion eingefügt). Bitte "token-hier-einfügen" und Domain bzw. Raum-ID anpassen (das "!" ist hier URL-encoded):
######################################################
# Sendet eine Matrix Message
# Aufruf: "{SendMatrixMessage($txt)}"
# Parameter: $txt: Die zu sendende Meldung
######################################################
sub SendMatrixMessage($)
{   
    my ($txt) = @_;
    my $token = "token-hier-einfügen";
    my $url = "https://matrix.meinedomain.de/_matrix/client/r0/rooms/%21abcxyz:matrix.meinedomain.de/send/m.room.message?access_token=" . $token;
    my $msg = '{"msgtype":"m.text", "body":"' . $txt . '"}';
    my $header = "Content-Type: application/json; charset=UTF-8";

my $param = {
url => $url,
method => "POST",
header => $header,
data => $msg,
noshutdown => 0,
callback => sub($$$)
{
my ($param, $err, $data) = @_;
Log(1, 'SendMatrixMessage ' . ($err ne '' ? 'Error: Could not send message: ' . $err : (index($data, "event_id") == -1 ? 'Error: Could not send message: ' . $data : 'successfully')));
},
    };

    HttpUtils_NonblockingGet($param);
    return 0;
}
# End SendMatrixMessage


Aufgerufen wird das ganze nun z.B. über die FHEM-Eingabemaske (kann dann aber auch in Notifies, o.ä. genutzt werden):
{SendMatrixMessage("test")}

Nun sollte im entsprechenden Raum eine Meldung ankommen.

Einschränkung ist hier, dass die Meldungen unverschlüsselt versendet werden (auch wenn der Raum eigentlich mit Verschlüsselung arbeitet). Mit der Verschlüsselung sind hier wohl noch ein paar mehr Schritte notwendig, wo ich noch nicht ganz durchgestiegen bin.

Ja, ich weiß: Die Sache ist äußerst "hemdsärmelig". Trotzdem könnte das Code-Schnipsel vielleicht auch für andere interessant sein.
Fragen oder Anmerkungen? Immer her damit!  ;)

Edit: Umgebaut auf "HttpUtils_NonblockingGet", ansonsten war der HTTP-Request blocking. Danke an CoolTux für den Hinweis.

BOFH

ZitatDie Authentifizierung geschieht hier mittels Token. Dieses Token (vom Matrix-FHEM-Account) bekommt ihr direkt im Client (Browser, Desktop, etc.) unter "Alle Einstellungen" > "Hilfe und Über" > ganz runter scrollen und hier auf "Zugangstoken" klicken.

Ich finde das leider unter iOS nicht. ,,Hilfe und über" sehe ich nicht.
Nutze Element als client

Danke für info

P.s. Tolle idee
RasPi 4
ZWave.me ZME_UZB (Fibaro Auge Gen.2)/ HM-USB2 (Thermostat | Hutschienen Relais | 1-/2fach Schalter) / Enigma2 / PhilipsTV / Philips HUE (GO|Bulb|Stripe (plus)) / Somfy IO Rollos / BOSCH HSG636XS6 / SONOS (P1, P3, P5 2.Gen, SUB, Bar)

DecaTec

Zitat von: BOFH am 03 Mai 2021, 13:58:08
Ich finde das leider unter iOS nicht. ,,Hilfe und über" sehe ich nicht.

Stimmt, da scheint es das nicht zu geben. Hier einfach einmal per Element/Web einloggen (entweder über den offiziellen Client, oder eine beliebige anderen Element/Web-Instanz).

Ansonsten tut es auch so etwas:
curl -XPOST -d '{"type":"m.login.password", "user":"USER", "password":"PASSWORT"}' "https://matrix.meinedomain.de/_matrix/client/r0/login"
Das spuckt dir dann auch das Token aus.

CoolTux

Schau mal bitte ob der Code geht.
Dann sparst die "HandleMatrixResponse" Funktion

my $param = {
        url => $url,
        method => "POST",
        header => $header,
        data => $msg,
        noshutdown => 0,
        callback => \&sub() { my (undef, $err, $data) = @_; Log(1, 'SendMatrixMessage  ' . ($err ne '' ? 'Error: Could not send message: ' . $err : (index($data, "event_id") == -1 ? 'Error: Could not send message: ' . $data : 'successfully') ); },
    };
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

DecaTec

#4
Ja, das ginge sicher auch, aber wie sieht das denn aus?  :P

Da hab ich dann lieber eine Callback-Funktion.
Sorry, bin kein Fan von solchen Inline-Definitionen.

Edit: Wenn man es ein wenig formatiert, dann isses kein One-Liner mehr und etwas übersichtlicher. Änderungsvorschlag akzeptiert.  ;)

Carsten K.

Gibt es auch einen Weg, um matrix-Nachrichten in FHEM zu empfangen/verarbeiten?
NUC FHEM on Debian, CC1101-USB-Lite 868MHz;
HM_HM_CC_RT_DN, HM-LC-SW1-PL2, HM_HM_TC_IT_WM_W_EU, HM-SEC-SC-2, HM-ES-TX-WM
FRITZ!DECT 200
Philips TV (Android), VuDuo2, VU Ultimo4k

DecaTec

Zitat von: Carsten K. am 08 Oktober 2023, 17:57:17Gibt es auch einen Weg, um matrix-Nachrichten in FHEM zu empfangen/verarbeiten?

Das wäre technisch sicherlich auch möglich, aber dazu müsste man wohl ein komplettes Modul basteln (wie es dies z.B. für Telegram gibt), dann könnte man sicherlich die Kommunikation in "beide Richtungen" abbilden.

Das Code-Snippet von oben ist aber wirklich nur dazu da, um schnell eine Nachricht FHEM -> Matrix zu senden.

CoolTux

Es gibt da ein inoffiziell Modul von jemandem aus dem Forum.
Ich hatte daran auch noch etwas gearbeitet und die Änderungen dem jenigen zukommen lassen. Das Modul mit meinen Änderungen verrichtet hier seine Arbeit. Und ich glaube es kann sogar Nachrichten empfangen. Nutze ich nur nicht.
Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

Carsten K.

Vielen Dank für Eure Antworten...
Ich hatte gehofft, hier kurzfristig eine Alternative zu Signal zu finden, da die Signal-Installation (zumindest bei mir) nicht immer reibungslos läuft.
Ich kann mir die erforderlichen Schritte für eine Matrix-Integration grob vorstellen und halte den benötigten Zeitaufwand (Bauchgefühl) für recht hoch.

Schönen Tag noch  :)
NUC FHEM on Debian, CC1101-USB-Lite 868MHz;
HM_HM_CC_RT_DN, HM-LC-SW1-PL2, HM_HM_TC_IT_WM_W_EU, HM-SEC-SC-2, HM-ES-TX-WM
FRITZ!DECT 200
Philips TV (Android), VuDuo2, VU Ultimo4k

Torxgewinde

#9
Hi,
Ich hatte da auch mal reingeschnuppert und wollte als Snippet folgendes BASH-Script fallen lassen. Es sendet eine Textnachricht und eine Bildnachricht und ich weiß noch nicht ob und ggf. wann ich daran weiterbastel. NTFY erfüllt meine Anforderungen bisher sehr gut, aber Matrix hatte mich einfach mal interessiert. Ich nutze es täglich als Brücke zu dem in DE leider immer noch recht verbreitetem WhatsApp. Um nicht die WA-App in meinem Hauptprofil zu haben kommen mir nur Opensource-Apps in Sinn. WA-App läuft deswegen nur auf einem Burnerphone und wird alles 14 Tage zum Refresh einmal aktiviert. Egal, hier der Snippet der sich den Token selbst besorgt:

#!/bin/bash

SERVER="https://nope.chat"
USER="DeinUsername"
PASS="Password123"
ROOM_ID="!1234567890123456:nope.chat" # find it at Room Settings --> Advanced --> Internal room ID
ATTACHMENT="./output.gif"

# Get Matrix versions
#VERSIONS=$(curl -s "${SERVER}/_matrix/client/versions" | jq '.')

# Try v3 login first
RESPONSE=$(curl -s -X POST "${SERVER}/_matrix/client/v3/login" \
     -H "Content-Type: application/json" \
     -d "{
           \"type\": \"m.login.password\",
           \"identifier\": {
             \"type\": \"m.id.user\",
             \"user\": \"${USER}\"
           },
           \"password\": \"${PASS}\"
         }")

# Extract and save access token
TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')
if [[ "$TOKEN" != "null" ]]; then
    echo "Access Token: $TOKEN"
else
    echo "Login failed!"
    exit 1
fi

if [ "$ROOM_ID" == "" ]; then
  echo "You are in the following rooms:"
  echo "---------------------------------------------------------------"
  for ID in $(curl -s -X GET "${SERVER}/_matrix/client/v3/joined_rooms" \
        -H "Authorization: Bearer ${TOKEN}" | jq -r '.joined_rooms[]'); do
    NAME=$(curl -s -X GET "${SERVER}/_matrix/client/v3/rooms/${ID}/state/m.room.name" \
        -H "Authorization: Bearer ${TOKEN}" | jq -r '.name // "<no name>"')
    echo "$NAME => $ID"
    echo "check if your ROOM_ID is there ------------------------------"
    exit 0
  done
fi

MESSAGE="Hello from Bash script!"
TXN_ID=$(date +%s)  # Unique transaction ID

curl -s -X PUT "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \
     -H "Authorization: Bearer ${TOKEN}" \
     -H "Content-Type: application/json" \
     -d "{
           \"msgtype\": \"m.text\",
           \"body\": \"${MESSAGE}\"
         }"

# Get the MIME type of the attachment
MIME_TYPE=$(file --mime-type -b "$ATTACHMENT")

# Upload the image
UPLOAD_RESPONSE=$(curl -s -X POST "${SERVER}/_matrix/media/v3/upload" \
     -H "Authorization: Bearer ${TOKEN}" \
     -H "Content-Type: $MIME_TYPE" \
     --data-binary @"$ATTACHMENT")

# Extract the Matrix content URL
CONTENT_URL=$(echo "$UPLOAD_RESPONSE" | jq -r '.content_uri')

if [[ "$CONTENT_URL" == "null" || -z "$CONTENT_URL" ]]; then
    echo "Image upload failed: $UPLOAD_RESPONSE"
    exit 1
fi

TXN_ID=$(date +%s)  # Unique transaction ID

RESPONSE=$(curl -s -X PUT "${SERVER}/_matrix/client/v3/rooms/${ROOM_ID}/send/m.room.message/${TXN_ID}" \
     -H "Authorization: Bearer ${TOKEN}" \
     -H "Content-Type: application/json" \
     -d "{
           \"msgtype\": \"m.image\",
           \"body\": \"$(basename "$ATTACHMENT")\",
           \"url\": \"$CONTENT_URL\",
           \"info\": {
               \"mimetype\": \"$MIME_TYPE\",
               \"size\": $(stat -c%s "$ATTACHMENT")
           }
         }")

# Check for success
if [[ $(echo "$RESPONSE" | jq -r '.event_id') != "null" ]]; then
    echo "Image sent successfully!"
else
    echo "Failed to send image: $RESPONSE"
    exit 1
fi

Torxgewinde

#10
Als HTTPMOD kann man so bereits eine Nachricht versenden:

defmod MatrixBot HTTPMOD none 0
attr MatrixBot userattr MatrixPassword MatrixRoomID MatrixServer MatrixUser
attr MatrixBot MatrixRoomID !abcddefghijklmn:nope.chat
attr MatrixBot MatrixServer https://nope.chat
attr MatrixBot MatrixUser MeinNutzername
attr MatrixBot comment "\
The room-id can be found in Element-Web at:\
Room Settings --> Advanced --> Internal room ID\
\
To set the password:\
set MatrixBot storeKeyValue MatrixPassword yourPassword123 \
"
attr MatrixBot get01Data {\
  "type": "m.login.password",\
  "identifier": {\
    "type": "m.id.user",\
    "user": "[$name:MatrixUser]"\
  },\
  "password": "%%MatrixPassword%%"\
}
attr MatrixBot get01HeaderContent-Type application/json
attr MatrixBot get01Name token
attr MatrixBot get01Regex "access_token"\s*:\s*"([^"]+)"
attr MatrixBot get01TextArg 0
attr MatrixBot get01URL [$name:MatrixServer]/_matrix/client/v3/login
attr MatrixBot replacement01Mode expression
attr MatrixBot replacement01Regex \[([^:]+):([^\]]+)\]
attr MatrixBot replacement01Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr MatrixBot replacement02Mode expression
attr MatrixBot replacement02Regex %%uuid%%
attr MatrixBot replacement02Value join("-", unpack("A8 A4 A4 A4 A12", unpack("H*", join("", map { chr(int rand 256) } 0..15))))
attr MatrixBot replacement03Mode key
attr MatrixBot replacement03Regex %%MatrixPassword%%
attr MatrixBot replacement03Value MatrixPassword
attr MatrixBot set01Data {\
  "msgtype": "m.text",\
  "body": "$val"\
}
attr MatrixBot set01HeaderAuthorization Authorization: Bearer [$name:token]
attr MatrixBot set01HeaderContent-Type application/json
attr MatrixBot set01Method POST
attr MatrixBot set01Name sendText
attr MatrixBot set01TextArg 1
attr MatrixBot set01URL [$name:MatrixServer]/_matrix/client/v3/rooms/[$name:MatrixRoomID]/send/m.room.message?txnId=%%uuid%%
attr MatrixBot verbose 1

Das Passwort wird verschleiert gespeichert, damit man es setzt muss man:
set MatrixBot storeKeyValue MatrixPassword yourPassword123

Um dann einen Access-Token anzufordern (muss man nur ganz selten machen sobald man einen Token hat):
get MatrixBot token
Es wird dann das Reading token gesetzt

Zum Senden einer Textnachricht:
set MatrixBot sendText Mein Test Text kommt hier hin

CoolTux

Du musst nicht wissen wie es geht! Du musst nur wissen wo es steht, wie es geht.
Support me to buy new test hardware for development: https://www.paypal.com/paypalme/MOldenburg
My FHEM Git: https://git.cooltux.net/FHEM/
Das TuxNet Wiki:
https://www.cooltux.net

Torxgewinde

#12
@CoolTux: Danke für das Modul.

Ich dachte ich mach' es mir "einfach" und nur das Nötigste "mal kurz" in HTTPMOD und es war wie so oft viel aufwendiger als ich Anfangs dachte - hätte ich mal einfach das Modul genommen :-)

Wie auch immer, hier ein HTTPMOD Device mit dem man Nachrichten senden kann und auch longpoll auf Nachrichten in einem Raum ausführen kann (Matrix kann keine Websockets). Gibt noch viel zu tun, aber ich wollte den aktuellen Stand teilen:

defmod MatrixBot HTTPMOD none 0
attr MatrixBot userattr MatrixPassword MatrixRoomID MatrixServer MatrixUser
attr MatrixBot MatrixRoomID !123456789EHIbAYFYh:nope.chat
attr MatrixBot MatrixServer nope.chat
attr MatrixBot MatrixUser meinNutzerName
attr MatrixBot bodyDecode utf-8
attr MatrixBot comment "\
Create a room for FHEM.\
\
The room must not use encryption, a room that has encryption\
enabled, cannot be converted to a non-encrypted room anymore \
\
The room-id can be found in Element-Web at:\
Room Settings --> Advanced --> Internal room ID\
\
To store the password in FHEM in obfuscated way:\
set MatrixBot storeKeyValue MatrixPassword yourPassword123\
\
###\
To send a text:\
# 1. obtain token (if not already there):\
get MatrixBot token\
\
# 2. send Text\
set MatrixBot sendText Bla Bla Bla\
\
\
###\
To longpoll for messages:\
# 1. obtain token (if not already there):\
get MatrixBot token\
\
# 2. send special filter to Matrix:\
set MatrixBot sendFilter\
\
# 3. enable longPoll (waits up to 60 seconds\
#                     or until data is available)\
get MatrixBot longpoll\
\
#############################################################\
https://spec.matrix.org/v1.14/client-server-api/#syncing\
"
attr MatrixBot get01Data {\
  "type": "m.login.password",\
  "identifier": {\
    "type": "m.id.user",\
    "user": "[$name:MatrixUser]"\
  },\
  "password": "%%MatrixPassword%%"\
}
attr MatrixBot get01HeaderContent-Type application/json
attr MatrixBot get01Name token
attr MatrixBot get01Regex "access_token"\s*:\s*"([^"]+)"
attr MatrixBot get01TextArg 0
attr MatrixBot get01URL https://[$name:MatrixServer]/_matrix/client/v3/login
attr MatrixBot get02AlwaysNum 0
attr MatrixBot get02DeleteIfUnmatched 1
attr MatrixBot get02HeaderAuthorization Authorization: Bearer [$name:token]
attr MatrixBot get02Name longpoll
attr MatrixBot get02Regex \"next_batch\":\s*\"(?<next_batch>[^\"]+)\"(?:.*?\"timeline\":\s*{\s*\"events\":\s*\[\s*(?<events_json>.*?)(?=\s*\]\s*)\s*)?
attr MatrixBot get02TextArg 0
attr MatrixBot get02URL https://[$name:MatrixServer]/_matrix/client/v3/sync?timeout=60000&filter=[$name:filter_id]%%next_batch_param%%
attr MatrixBot replacement01Mode expression
attr MatrixBot replacement01Regex \[([^:\s\[\"\']+):([^\]\s]+)\]
attr MatrixBot replacement01Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr MatrixBot replacement02Mode expression
attr MatrixBot replacement02Regex %%uuid%%
attr MatrixBot replacement02Value join("-", unpack("A8 A4 A4 A4 A12", unpack("H*", join("", map { chr(int rand 256) } 0..15))))
attr MatrixBot replacement03Mode key
attr MatrixBot replacement03Regex %%MatrixPassword%%
attr MatrixBot replacement03Value MatrixPassword
attr MatrixBot replacement04Mode expression
attr MatrixBot replacement04Regex %%next_batch_param%%
attr MatrixBot replacement04Value #is there a reading 'next_batch'?\
my $val = ReadingsVal($name, 'next_batch', '???');;\
\
#return the GET parameter 'sync=value' for /sync Endpoint\
return "&since=$val" if ($val ne '???');;\
\
#return neither since-key nor value for it:\
return "";;
attr MatrixBot set01Data {\
  "msgtype": "m.text",\
  "body": "$val"\
}
attr MatrixBot set01HeaderAuthorization Authorization: Bearer [$name:token]
attr MatrixBot set01HeaderContent-Type application/json
attr MatrixBot set01Method POST
attr MatrixBot set01Name sendText
attr MatrixBot set01TextArg 1
attr MatrixBot set01URL https://[$name:MatrixServer]/_matrix/client/v3/rooms/[$name:MatrixRoomID]/send/m.room.message?txnId=%%uuid%%
attr MatrixBot set02Data {\
  "room": {\
    "rooms": ["[$name:MatrixRoomID]"],\
    "timeline": {\
      "limit": 10,\
      "types": ["m.room.message"]\
    },\
    "include_leave": false,\
    "include_join": false,\
    "include_account_data": false,\
    "include_state": false,\
    "state": {\
      "types": []\
    },\
    "ephemeral": {\
      "types": []\
    },\
    "account_data": {\
      "types": []\
    }\
  },\
  "event_fields": [\
    "content.body",\
    "sender",\
    "origin_server_ts"\
  ],\
  "event_format": "client",\
  "presence": {\
    "types": []\
  },\
  "account_data": {\
    "types": []\
  }\
}
attr MatrixBot set02HeaderAuthorization Authorization: Bearer [$name:token]
attr MatrixBot set02HeaderContent-Type application/json
attr MatrixBot set02Method POST
attr MatrixBot set02Name sendFilter
attr MatrixBot set02NoArg 1
attr MatrixBot set02ParseResponse 1
attr MatrixBot set02Regex \"filter_id\":\s*\"(?<filter_id>\d+)\"
attr MatrixBot set02URL https://[$name:MatrixServer]/_matrix/client/v3/user/@[$name:MatrixUser]:[$name:MatrixServer]/filter
attr MatrixBot timeout 61
attr MatrixBot verbose 1

Torxgewinde

#13
Hallo,
Dieses HTTPMOD kann Nachrichten an Matrix senden und auch verzögerungsarm empfangen (longpoll, websockets sind in Matrix nicht vorhanden).

Das Passwort wird verschleiert gespeichert, damit man es setzt muss man:
set MatrixBot storeKeyValue MatrixPassword yourPassword123

Zum Senden einer Textnachricht:
set MatrixBot sendText Mein Test Text kommt hier hin
Nachrichten kann man an verschlüsselte als auch unverschlüsselte Räume senden. Lediglich, wenn verschlüsselte Räume es verbieten, kann man hiermit keine Nachricht senden. Es wird immer HTTPS als Transportverschlüsselung genutzt, das Megolm-E2EE-Protokoll allerdings nicht.

Zum Empfangen von Nachrichten legt man einen unverschlüsselten Raum an und kann dann so auf Nachrichten lauschen:
set MatrixBot longpollCmd startTimer
Eintreffende Nachrichten können "im Schwung" eintreffen. Damit FHEM diese Nachrichten einzeln sieht, erzeugt das Device Einzel-Events, die im Eventmonitor so aussehen:
2025-04-11 20:59:13 HTTPMOD MatrixBot msg: 2025-04-11 20:58:56: @MeinNutzerName:nope.chat: Nachricht 1
2025-04-11 20:59:13 HTTPMOD MatrixBot msg: 2025-04-11 20:59:04: @MeinNutzerName:nope.chat: und 2
2025-04-11 20:59:13 HTTPMOD MatrixBot msg: 2025-04-11 20:59:04: @MeinNutzerName:nope.chat: 3 und
2025-04-11 20:59:13 HTTPMOD MatrixBot msg: 2025-04-11 20:59:05: @MeinNutzerName:nope.chat: 4
So kann man dann mit DOIF, Notify etc auf bestimmte Nachrichten lauschen.

Unicode 🚀🤩 und Umlaute äöü funktionieren natürlich auch.

defmod MatrixBot HTTPMOD none 0
attr MatrixBot userattr MatrixRoomID MatrixServer MatrixUser
attr MatrixBot MatrixRoomID !1234567890:nope.chat
attr MatrixBot MatrixServer nope.chat
attr MatrixBot MatrixUser DeinNutzername
attr MatrixBot bodyDecode utf-8
attr MatrixBot comment "\
Create a room for FHEM.\
\
The room must not use encryption, a room that has encryption\
enabled, cannot be converted to a non-encrypted room anymore \
\
The room-id can be found in Element-Web at:\
Room Settings --> Advanced --> Internal room ID\
\
To store the password in FHEM in obfuscated way:\
set MatrixBot storeKeyValue MatrixPassword yourPassword123\
\
###\
To send a text:\
set MatrixBot sendText Bla Bla Bla\
\
\
###\
To longpoll for messages once:\
# 1. send special filter to Matrix:\
set MatrixBot sendFilter\
\
# 2. start one longPoll (waits up to 60 seconds\
#                        or until data is available)\
get MatrixBot longpoll\
\
#alternatively, to keep on longPolling (this also sets filter):\
set MatrixBot longpollCmd startTimer\
#to stop the timers:\
set MatrixBot longpollCmd stopTimer\
\
#############################################################\
https://spec.matrix.org/v1.14/client-server-api/#syncing\
"
attr MatrixBot get02AlwaysNum 0
attr MatrixBot get02HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot get02Name longpoll
attr MatrixBot get02Regex \"next_batch\":\s*\"(?<next_batch>[^\"]+)\"(?:.*?\"timeline\":\s*{\s*\"events\":\s*(?<messages>\[.*?\])\s*)?
attr MatrixBot get02TextArg 0
attr MatrixBot get02URL https://[$name:MatrixServer]/_matrix/client/v3/sync?timeout=60000&filter=[$name:filter_id]%%next_batch_param%%
attr MatrixBot reAuthRegex M_UNKNOWN_TOKEN
attr MatrixBot replacement01Mode expression
attr MatrixBot replacement01Regex \[([^:\s\[\"\']+):([^\]\s]+)\]
attr MatrixBot replacement01Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr MatrixBot replacement02Mode expression
attr MatrixBot replacement02Regex %%uuid%%
attr MatrixBot replacement02Value join("-", unpack("A8 A4 A4 A4 A12", unpack("H*", join("", map { chr(int rand 256) } 0..15))))
attr MatrixBot replacement03Mode key
attr MatrixBot replacement03Regex %%MatrixPassword%%
attr MatrixBot replacement03Value MatrixPassword
attr MatrixBot replacement04Mode expression
attr MatrixBot replacement04Regex %%next_batch_param%%
attr MatrixBot replacement04Value #is there a reading 'next_batch'?\
my $val = ReadingsVal($name, 'next_batch', '???');;\
\
#return the GET parameter 'sync=value' for /sync Endpoint\
return "&since=$val" if ($val ne '???');;\
\
#return neither since-key nor value for it:\
return "";;
attr MatrixBot set01Data {\
  "msgtype": "m.text",\
  "body": "$val"\
}
attr MatrixBot set01HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot set01HeaderContent-Type application/json
attr MatrixBot set01Method POST
attr MatrixBot set01Name sendText
attr MatrixBot set01TextArg 1
attr MatrixBot set01URL https://[$name:MatrixServer]/_matrix/client/v3/rooms/[$name:MatrixRoomID]/send/m.room.message?txnId=%%uuid%%
attr MatrixBot set02Data {\
  "room": {\
    "rooms": ["[$name:MatrixRoomID]"],\
    "timeline": {\
      "limit": 10,\
      "types": ["m.room.message"]\
    },\
    "include_leave": false,\
    "include_join": false,\
    "include_account_data": false,\
    "include_state": false,\
    "state": {\
      "types": []\
    },\
    "ephemeral": {\
      "types": []\
    },\
    "account_data": {\
      "types": []\
    }\
  },\
  "event_fields": [\
    "content.body",\
    "sender",\
    "origin_server_ts"\
  ],\
  "event_format": "client",\
  "presence": {\
    "types": [],\
    "not_types": ["*"]\
  },\
  "account_data": {\
    "types": [],\
    "not_types": ["*"]\
  }\
}
attr MatrixBot set02HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot set02HeaderContent-Type application/json
attr MatrixBot set02Method POST
attr MatrixBot set02Name sendFilter
attr MatrixBot set02NoArg 1
attr MatrixBot set02ParseResponse 1
attr MatrixBot set02Regex \"filter_id\":\s*\"(?<filter_id>\d+)\"
attr MatrixBot set02URL https://[$name:MatrixServer]/_matrix/client/v3/user/@[$name:MatrixUser]:[$name:MatrixServer]/filter
attr MatrixBot set03Local 1
attr MatrixBot set03Name longpollCmd
attr MatrixBot set03TextArg 1
attr MatrixBot showBody 0
attr MatrixBot showError 1
attr MatrixBot sid01Data {\
  "type": "m.login.password",\
  "identifier": {\
    "type": "m.id.user",\
    "user": "[$name:MatrixUser]"\
  },\
  "password": "%%MatrixPassword%%"\
}
attr MatrixBot sid01HeaderContent-Type application/json
attr MatrixBot sid01IdRegex "access_token"\s*:\s*"([^"]+)"
attr MatrixBot sid01URL https://[$name:MatrixServer]/_matrix/client/v3/login
attr MatrixBot timeout 62
attr MatrixBot userReadings longpollTimer:(next_batch|longpollCmd|LAST_ERROR):.* {\
  my $longpollCmd = ReadingsVal($name, 'longpollCmd', '???');;\
  my $delay = 1;;\
  my $timeout = AttrVal($name, 'timeout', 61) + $delay + 5;;\
  \
  # stop our timers:\
  if ($longpollCmd eq "stopTimer") {\
    fhem("cancel ${name}_longpollTimer quiet");;\
    fhem("cancel ${name}_longpollTimer2 quiet");;\
    return "stopped";;\
  }\
  \
  if ($longpollCmd ne "startTimer") {\
    return "no timer set, longpollCmd is not set to 'startTimer'";;\
  }\
  \
  #if startTimer cmd was given now, set filter as well:\
  if (ReadingsAge($name, 'longpollCmd', 0) <= 1) {\
    $delay = 5;; #delay to allow for sendFilter to be answered\
    #Log(1, "🪲 $name: >>". InternalVal($name, 'httpbody', '???') ."<<");;\
    fhem("sleep 0.1 quiet;; set $name sendFilter");;\
  }\
  \
  #we handle an error reported by http-utils:\
  if (ReadingsAge($name, 'LAST_ERROR', 0) <= 1) {\
    my $last_error = ReadingsVal($name, 'LAST_ERROR', '???');;\
    $delay = 10;; #delay to allow for error reasons to improve\
    #Log(1, "🪲 $name: Dealing with error: >>$last_error<<");;\
  }\
  \
  #set timers, one regular and one fallback:\
  fhem("sleep $delay ${name}_longpollTimer quiet;; get $name longpoll");;\
  fhem("sleep $timeout ${name}_longpollTimer2 quiet;; set $name longpollCmd startTimer");;\
  \
  return strftime("next longpoll at %H:%M:%S", localtime( time()+$delay ));;\
},\
messages_list:messages:.* {\
  my $this = ReadingsVal($name, $reading, '');;\
  my @timestampArray = split("\n", $this);;\
  my $messages_ref = decode_json(ReadingsVal($name, 'messages', ''));;\
  my $length = 20;;\
  \
  my %seen_messages = map { $_ => 1 } @timestampArray;;\
  \
  foreach my $msg (@$messages_ref) {\
    my $val = $msg->{content}{body};;\
    $val = Encode::decode('utf-8', $val) unless Encode::is_utf8($val);;\
    \
    my $sender = $msg->{sender};;\
    my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime($msg->{origin_server_ts} / 1000));;\
    my $new_entry = "$ts: $sender: $val";;\
    \
    next if $seen_messages{$new_entry};;\
    \
    my $inserted = 0;;\
    for (my $i = 0;; $i < @timestampArray;; $i++) {\
      my ($existing_ts) = $timestampArray[$i] =~ /^([^:]+):/;;\
      if ($ts lt $existing_ts) {\
        splice(@timestampArray, $i, 0, $new_entry);;\
        $inserted = 1;;\
        last;;\
      }\
    }\
    push(@timestampArray, encode('utf-8', $new_entry)) unless $inserted;;\
    shift(@timestampArray) while @timestampArray > $length;;\
  }\
  \
  return join("\n", @timestampArray);;\
},\
process_messages:messages:.* {\
  my $val = ReadingsVal($name, 'messages_list', '');;\
  my $this = ReadingsVal($name, $reading, '');;\
  my $latest_ts = $this;;\
  \
  foreach my $line (split(/\n/, $val)) {\
    if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):\s*(.*)$/) {\
      my ($ts, $msg) = ($1, $2);;\
      \
      if ($ts gt $this) {\
        fhem("trigger $name msg: $line");;\
        $latest_ts = $ts if ($ts gt $latest_ts);;\
      }\
    }\
  }\
  return $latest_ts;;\
}
attr MatrixBot verbose 3
attr MatrixBot widgetOverride longpollCmd:uzsuSelectRadio,startTimer,stopTimer

Ein einfaches DOIF kann zum Beispiel auf die Nachricht "Ping" oder auch "ping" mit einem "Pong!" reagieren:
defmod MatrixPing.doif DOIF (["MatrixBot:msg: .*: @.*: [Pp]ing$"])\
  (set MatrixBot sendText Pong!)
attr MatrixPing.doif do always
attr MatrixPing.doif icon homeConnect
attr MatrixPing.doif alias Matrix Ping/Pong

Falls es gefällt, freue ich mich über einen Daumen hoch.

Bekannte Probleme: Wenn HTTPMOD bereits auf Nachrichten lauscht und man sendet dann Nachrichten, wird der Empfang bis zum Timeout unterbrochen und synchronisiert sich erst später. Wenn das stört, kann man einfach ein Device zum Senden anlegen und eins für den Empfang.

So kann es aussehen:
Du darfst diesen Dateianhang nicht ansehen.

Torxgewinde

#14
Hi,
Noch eine Detailverbesserung: Ich habe noch eine parseFunction1 ergänzt, falls der Server einen HTTP-Code von 401, 403 oder 500 sendet wird der Access-Token erneuert:

if (!defined &HTTPMOD::handleAuthErrors) {
    *HTTPMOD::handleAuthErrors = sub {
      my ($hash, $header, $body, $request) = @_;
      my $name = $hash->{NAME};
      my $status;

      if ($header =~ m{^HTTP/\d\.\d\s+(\d+)}m) {
        $status = $1;
      }

      Log3($name, 4, "$name: HTTP status code is $status");

      if ( $status == 401 || $status == 403 || $status == 500 ) {
        Log3($name, 3, "$name: auth-error or servererror ($status), calling doAuth()");
        HTTPMOD::DoAuth($hash);
      }
    };
  }

als komplettes Listing dann:
defmod MatrixBot HTTPMOD none 0
attr MatrixBot userattr MatrixRoomID MatrixServer MatrixUser
attr MatrixBot MatrixRoomID !12345678901234:nope.chat
attr MatrixBot MatrixServer nope.chat
attr MatrixBot MatrixUser DeinNutzername
attr MatrixBot bodyDecode utf-8
attr MatrixBot comment "\
Create a room for FHEM.\
\
The room must not use encryption, a room that has encryption\
enabled, cannot be converted to a non-encrypted room anymore \
\
The room-id can be found in Element-Web at:\
Room Settings --> Advanced --> Internal room ID\
\
To store the password in FHEM in obfuscated way:\
set MatrixBot storeKeyValue MatrixPassword yourPassword123\
\
###\
To send a text:\
set MatrixBot sendText Bla Bla Bla\
\
\
###\
To longpoll for messages once:\
# 1. send special filter to Matrix:\
set MatrixBot sendFilter\
\
# 2. start one longPoll (waits up to 60 seconds\
#                        or until data is available)\
get MatrixBot longpoll\
\
#alternatively, to keep on longPolling (this also sets filter):\
set MatrixBot longpollCmd startTimer\
#to stop the timers:\
set MatrixBot longpollCmd stopTimer\
\
#############################################################\
https://spec.matrix.org/v1.14/client-server-api/#syncing\
"
attr MatrixBot get02AlwaysNum 0
attr MatrixBot get02HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot get02Name longpoll
attr MatrixBot get02Regex \"next_batch\":\s*\"(?<next_batch>[^\"]+)\"(?:.*?\"timeline\":\s*{\s*\"events\":\s*(?<messages>\[.*?\])\s*)?
attr MatrixBot get02TextArg 0
attr MatrixBot get02URL https://[$name:MatrixServer]/_matrix/client/v3/sync?timeout=60000&filter=[$name:filter_id]%%next_batch_param%%
attr MatrixBot icon message_info
attr MatrixBot parseFunction1 handleAuthErrors
attr MatrixBot reAuthAlways 0
attr MatrixBot reAuthRegex M_UNKNOWN_TOKEN
attr MatrixBot replacement01Mode expression
attr MatrixBot replacement01Regex \[([^:\s\[\"\']+):([^\]\s]+)\]
attr MatrixBot replacement01Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr MatrixBot replacement02Mode expression
attr MatrixBot replacement02Regex %%uuid%%
attr MatrixBot replacement02Value join("-", unpack("A8 A4 A4 A4 A12", unpack("H*", join("", map { chr(int rand 256) } 0..15))))
attr MatrixBot replacement03Mode key
attr MatrixBot replacement03Regex %%MatrixPassword%%
attr MatrixBot replacement03Value MatrixPassword
attr MatrixBot replacement04Mode expression
attr MatrixBot replacement04Regex %%next_batch_param%%
attr MatrixBot replacement04Value #is there a reading 'next_batch'?\
my $val = ReadingsVal($name, 'next_batch', '???');;\
\
#return the GET parameter 'sync=value' for /sync Endpoint\
return "&since=$val" if ($val ne '???');;\
\
#return neither since-key nor value for it:\
return "";;
attr MatrixBot set01Data {\
  "msgtype": "m.text",\
  "body": "$val"\
}
attr MatrixBot set01HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot set01HeaderContent-Type application/json
attr MatrixBot set01Method POST
attr MatrixBot set01Name sendText
attr MatrixBot set01TextArg 1
attr MatrixBot set01URL https://[$name:MatrixServer]/_matrix/client/v3/rooms/[$name:MatrixRoomID]/send/m.room.message?txnId=%%uuid%%
attr MatrixBot set02Data {\
  "room": {\
    "rooms": ["[$name:MatrixRoomID]"],\
    "timeline": {\
      "limit": 10,\
      "types": ["m.room.message"]\
    },\
    "include_leave": false,\
    "include_join": false,\
    "include_account_data": false,\
    "include_state": false,\
    "state": {\
      "types": []\
    },\
    "ephemeral": {\
      "types": []\
    },\
    "account_data": {\
      "types": []\
    }\
  },\
  "event_fields": [\
    "content.body",\
    "sender",\
    "origin_server_ts"\
  ],\
  "event_format": "client",\
  "presence": {\
    "types": [],\
    "not_types": ["*"]\
  },\
  "account_data": {\
    "types": [],\
    "not_types": ["*"]\
  }\
}
attr MatrixBot set02HeaderAuthorization Authorization: Bearer $sid
attr MatrixBot set02HeaderContent-Type application/json
attr MatrixBot set02Method POST
attr MatrixBot set02Name sendFilter
attr MatrixBot set02NoArg 1
attr MatrixBot set02ParseResponse 1
attr MatrixBot set02Regex \"filter_id\":\s*\"(?<filter_id>\d+)\"
attr MatrixBot set02URL https://[$name:MatrixServer]/_matrix/client/v3/user/@[$name:MatrixUser]:[$name:MatrixServer]/filter
attr MatrixBot set03Local 1
attr MatrixBot set03Name longpollCmd
attr MatrixBot set03TextArg 1
attr MatrixBot showBody 0
attr MatrixBot showError 1
attr MatrixBot sid01Data {\
  "type": "m.login.password",\
  "identifier": {\
    "type": "m.id.user",\
    "user": "[$name:MatrixUser]"\
  },\
  "password": "%%MatrixPassword%%"\
}
attr MatrixBot sid01HeaderContent-Type application/json
attr MatrixBot sid01IdRegex "access_token"\s*:\s*"([^"]+)"
attr MatrixBot sid01URL https://[$name:MatrixServer]/_matrix/client/v3/login
attr MatrixBot timeout 65
attr MatrixBot userReadings longpollTimer:(next_batch|longpollCmd|LAST_ERROR):.* {\
  my $longpollCmd = ReadingsVal($name, 'longpollCmd', '???');;\
  my $delay = 1;;\
  my $timeout = AttrVal($name, 'timeout', 61) + $delay + 5;;\
\
  # stop our timers:\
  if ($longpollCmd eq "stopTimer") {\
    fhem("cancel ${name}_longpollTimer quiet");;\
    fhem("cancel ${name}_longpollTimer2 quiet");;\
    return "stopped";;\
  }\
  \
  if ($longpollCmd ne "startTimer") {\
    return "no timer set, longpollCmd is not set to 'startTimer'";;\
  }\
  \
  #if startTimer cmd was given now, set filter as well:\
  if (ReadingsAge($name, 'longpollCmd', 0) <= 1) {\
    $delay = 5;; #delay to allow for sendFilter to be answered\
    #Log(1, "🪲 $name: >>". InternalVal($name, 'httpbody', '???') ."<<");;\
    fhem("sleep 0.1 quiet;; set $name sendFilter");;\
  }\
  \
  #we handle an error reported by http-utils:\
  if (ReadingsAge($name, 'LAST_ERROR', 0) <= 1) {\
    my $last_error = ReadingsVal($name, 'LAST_ERROR', '???');;\
    $delay = 10;; #delay to allow for error reasons to improve\
    #Log(1, "🪲 $name: Dealing with error: >>$last_error<<");;\
  }\
  \
  # for testing this: { $defs{MatrixBot}{sid} = 'bla' }\
  if (!defined &HTTPMOD::handleAuthErrors) {\
    *HTTPMOD::handleAuthErrors = sub {\
      my ($hash, $header, $body, $request) = @_;;\
      my $name = $hash->{NAME};;\
      my $status;;\
      \
      if ($header =~ m{^HTTP/\d\.\d\s+(\d+)}m) {\
        $status = $1;;\
      }\
      \
      Log3($name, 4, "$name: HTTP status code is $status");;\
      \
      if ( $status == 401 || $status == 403 || $status == 500 ) {\
        Log3($name, 3, "$name: auth-error or servererror ($status), calling doAuth()");;\
        HTTPMOD::DoAuth($hash);;\
      }\
    };;\
  }\
  \
  #set timers, one regular and one fallback:\
  fhem("sleep $delay ${name}_longpollTimer quiet;; get $name longpoll");;\
  fhem("sleep $timeout ${name}_longpollTimer2 quiet;; set $name longpollCmd startTimer");;\
  \
  return strftime("next longpoll at %H:%M:%S", localtime( time()+$delay ));;\
},\
messages_list:messages:.* {\
  my $this = ReadingsVal($name, $reading, '');;\
  my @timestampArray = split("\n", $this);;\
  my $messages_ref = decode_json(ReadingsVal($name, 'messages', ''));;\
  my $length = 20;;\
  \
  my %seen_messages = map { $_ => 1 } @timestampArray;;\
  \
  foreach my $msg (@$messages_ref) {\
    my $val = $msg->{content}{body};;\
    $val = Encode::decode('utf-8', $val) unless Encode::is_utf8($val);;\
    \
    my $sender = $msg->{sender};;\
    my $ts = strftime("%Y-%m-%d %H:%M:%S", localtime($msg->{origin_server_ts} / 1000));;\
    my $new_entry = "$ts: $sender: $val";;\
    \
    next if $seen_messages{$new_entry};;\
    \
    my $inserted = 0;;\
    for (my $i = 0;; $i < @timestampArray;; $i++) {\
      my ($existing_ts) = $timestampArray[$i] =~ /^([^:]+):/;;\
      if ($ts lt $existing_ts) {\
        splice(@timestampArray, $i, 0, $new_entry);;\
        $inserted = 1;;\
        last;;\
      }\
    }\
    push(@timestampArray, encode('utf-8', $new_entry)) unless $inserted;;\
    shift(@timestampArray) while @timestampArray > $length;;\
  }\
  \
  return join("\n", @timestampArray);;\
},\
process_messages:messages:.* {\
  my $val = ReadingsVal($name, 'messages_list', '');;\
  my $this = ReadingsVal($name, $reading, '');;\
  my $latest_ts = $this;;\
  \
  foreach my $line (split(/\n/, $val)) {\
    if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}):\s*(.*)$/) {\
      my ($ts, $msg) = ($1, $2);;\
      \
      if ($ts gt $this) {\
        fhem("trigger $name msg: $line");;\
        $latest_ts = $ts if ($ts gt $latest_ts);;\
      }\
    }\
  }\
  return $latest_ts;;\
}
attr MatrixBot verbose 3
attr MatrixBot widgetOverride longpollCmd:uzsuSelectRadio,startTimer,stopTimer