Matrix
Allgemeines
Matrix ist ein offenes Kommunikationsprotokoll für dezentrale Echtzeitkommunikation. Es wird von zahlreichen Clients unterstützt, darunter Element, Element-X, SchildiChat Next, FluffyChat und viele weitere.
Viele Open-Source-Projekte betreiben eigene Chaträume auf föderierten Matrix-Servern. Ein Beispiel dafür ist der Gadgetbridge-Chat, einer der größeren und aktiveren Räume im Matrix-Ökosystem.
Matrix und FHEM
Um mit FHEM Nachrichten über das Matrix-Protokoll zu senden und zu empfangen, stehen mehrere Möglichkeiten zur Verfügung. Eine einfache und pragmatische Lösung ist die Nutzung eines HTTPMOD-Devices. Damit lassen sich Nachrichten über die Matrix-API versenden oder empfangen.
Matrix per HTTPMOD Device
Folgendes Device kann Matrix Nachrichten senden und empfangen, es hat wenige Abhängigkeiten und sollte in den meisten FHEM Installationen unkompliziert eingerichtet sein. Es wird die Datenübertragung von FHEM zum Server mit HTTPS verschlüsselt, E2EE wird nicht verwendet.
Einrichtung
Es sind die Attribute MatrixServer und MatrixUser zu setzen:
attr MatrixBot MatrixServer Dein-Servername
attr MatrixBot MatrixUser FHEM-Matrix-Username
Dann richtet man einen nicht E2E verschlüsselten Raum ein in dem der Matrix-FHEM-Account eingeladen ist. Von dem Raum benötigt man die MatrixRoomID, die man mit Element in den Raumdetails finden kann (Room Settings --> Advanced --> Internal room ID).
Das Passwort des Matrix-FHEM-Users setzt man mit dem Befehl:
set MatrixBot storeKeyValue MatrixPassword yourPassword123
Verwendung
Nachrichten senden geht mit:
set MatrixBot sendText Bla Bla Bla
Longpoll von Nachrichten starten und stoppen mit:
set MatrixBot longpollCmd startTimer
set MatrixBot longpollCmd stopTimer
Weiteres
https://forum.fhem.de/index.php?topic=120834.msg1339487#msg1339487
Device-Definition
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 DeinUsername
attr MatrixBot bodyDecode utf-8
attr MatrixBot bodyEncode 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 set01IExpr #cancel an active longpoll\
if ($hash->{BUSY}\
&& $hash->{HttpUtils}\
&& $hash->{HttpUtils}->{url} =~ m|^https://[^/]+/_matrix/client/v3/sync\?timeout=\d+&filter=| ) {\
Log(3, "$name: longpoll active, cutting it off now");;\
HttpUtils_Close($hash->{HttpUtils});;\
}\
\
#just return $val unteraltered\
$val
attr MatrixBot set01Method POST
attr MatrixBot set01Name sendText
attr MatrixBot set01ParseResponse 1
attr MatrixBot set01Regex {\"event_id\":\"(.*)\"}
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 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|sendText):.* {\
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);;\
$val =~ s/\n/ /g;; #replace newlines with spaces\
\
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