Ntfy
📬 NTFY.sh: Push Nachrichten an iOS, Android, PC, Command-Line, E-Mail & Telefon
NTFY.sh bietet Push-Dienste auf iOS, Android, Console, E-Mail, ja sogar als Anruf auf ein Telefon an. Es ist damit ähnlich zu dem was man mit PushOver, PushBullet oder auch Gotify anstellen kann, wobei NTFY auch Kriterien wie "Self-hosted" und echte "iOS-Pushnachrichten" erfüllt. Man kann damit von FHEM aus alle diese Kanäle mit einer Push-Nachricht erreichen. Die Nutzung ist extrem einfach, man setzt einen HTTP-Aufruf ab um eine Push-Nachricht zu versenden. NTFY.sh betreibt sogar einen Gratis-Server, den man nutzen darf. Man kann den Server auch selber hosten wenn man will.
Öffnet testweise diesen Link, dort tauchen die Nachrichten auch in der PWA (Webapp) auf: https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer
Anbindung per externem Modul
Um NTFY.sh einzurichten:
Code Auswählen
update add https://rm.byterazor.de/upd-fhem-ntfy/controls_byterazor-fhem-ntfy.txt
update
shutdown restart
Ein Device anlegen mit:
Code Auswählen
defmod NTFY0 NTFY_CLIENT https://ntfy.sh
attr NTFY0 defaultPriority default
attr NTFY0 defaultTopic FreundlichenGruesseAnAlleFHEMNutzer
attr NTFY0 room Experimente
Senden einer Nachricht mit einem curl-Befehl:
Code Auswählen
curl -d "Your message content here" \
-H "Title: Bla" \
https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer
Das Gleiche nur mit WGET:
Code Auswählen
wget --method=POST \
--body-data="Your message content here" \
--header="Title: Bla" \
https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer -q -O -
Oder, wenn es ein reines HTTP-GET als Webhook sein soll:
https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer/publish?message=Your%20message%20content%20here&title=Bla Oder, aus FHEM an den Server eine Nachricht senden mit:
Code Auswählen
set NTFY0 publish @FreundlichenGruesseAnAlleFHEMNutzer Testnachricht!
Im Event-Log taucht dann folgendes auf, darauf kann man mit notify oder DOIF reagieren:
Code Auswählen
2024-10-29 19:01:16.714 NTFY_TOPIC NTFY0_FreundlichenGruesseAnAlleFHEMNutzer nrReceivedMessages: 7
2024-10-29 19:01:16.715 NTFY_CLIENT NTFY0 subscriptions: FreundlichenGruesseAnAlleFHEMNutzer
2024-10-29 19:01:16.715 NTFY_CLIENT NTFY0 nrReceivedMessages: 7
2024-10-29 19:01:16.715 NTFY_CLIENT NTFY0 lastReceivedTitle: Bla
2024-10-29 19:01:16.715 NTFY_CLIENT NTFY0 lastReceivedData: Your message content here
2024-10-29 19:01:16.715 NTFY_CLIENT NTFY0 lastReceivedRawMessage: {"id":"oU5iqrdSFoeY","time":1730224876,"expires":1730268076,"event":"message","topic":"FreundlichenGruesseAnAlleFHEMNutzer","title":"Bla","message":"Your message content here"}
Testen kannst man hier:
https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer https://demo-fhem.cooltux.net/fhem?detail=NTFY0 <-- Begrenzt gültiger Link https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer/publish?message=Blubb&title=Bla
Anbindung per HTTPMOD und Websocket
Senden mit HTTPMOD
- set NTFY message Title="mein Titel aus dem Set heraus" Meine Nachricht
- set NTFY message Title="" Meine Nachricht ohne Titel
- set NTFY message Title="✅ Emojis 🚀" 📝 Meine Nachricht mit Emojis 👾
- oder man hat nur den Titel im User-Attribut "Title" falls das Muster Title="irgendwas" nicht im Parameter der set Nachricht auftaucht: set NTFY message Meine kurze Nachricht, Titel aus UserAttr Title genutzt
Empfangen mir Websocket
defmod NTFY_RECEIVE dummy
attr NTFY_RECEIVE userattr URL last_seen_max_age password username
attr NTFY_RECEIVE URL wss:ntfy.sh:443/FreundlichenGruesseAnAlleFHEMNutzer/ws
attr NTFY_RECEIVE alias NTFY_RECEIVE
attr NTFY_RECEIVE devStateIcon opened:general_ok@green:stop disconnected:rc_STOP@red:start
attr NTFY_RECEIVE eventMap /cmd connect:start/cmd disconnect:stop/
attr NTFY_RECEIVE group Experimente
attr NTFY_RECEIVE icon hue_filled_plug
attr NTFY_RECEIVE password superGeheimesPasswort
attr NTFY_RECEIVE readingList cmd
attr NTFY_RECEIVE setList cmd
attr NTFY_RECEIVE userReadings connect:cmd:.connect {\
my $hash = $defs{$name};;\
my $devState = DevIo_IsOpen($hash);;\
return "Device already open" if (defined($devState));;\
\
$hash->{DeviceName} = AttrVal($name, "URL", "wss:ntfy.sh:443/FreundlichenGruesseAnAlleFHEMNutzer/ws");;\
$hash->{DeviceName} =~ m,^(ws:|wss:)?([^/:]+):([0-9]+)(.*?)$,;;\
$hash->{header}{'Host'} = $2;;\
$hash->{header}{'User-Agent'} = 'FHEM';;\
\
my $user = AttrVal($name, "username", "???");;\
my $pwd = AttrVal($name, "password", "???");;\
if ($user ne "???" && $pwd ne "???") {\
my $encoded_auth = MIME::Base64::encode_base64("$user:$pwd", "");;\
$hash->{header}{'Authorization'} = "Basic $encoded_auth";;\
}\
\
$hash->{directReadFn} = sub () {\
my $hash = $defs{$name};;\
readingsBeginUpdate($hash);;\
my $buf = DevIo_SimpleRead($hash);;\
\
# track activity, emtpy buffer normally is from ping/pongs\
readingsBulkUpdate($hash, "last_seen", int(time()*1000));;\
RemoveInternalTimer($name.'Timeout');;\
my $timeoutFunction = sub() {\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $myCmd = ReadingsVal($name, "cmd", "disconnect");;\
return if ($myCmd =~ /disconnect|stop/);;\
\
Log3($name, 3, "$name: Timeout occured, restarting websocket...");;\
DevIo_CloseDev($hash);;\
readingsBeginUpdate($hash);;\
readingsBulkUpdate($hash, "state", "disconnected");;\
readingsBulkUpdate($hash, "cmd", "connect", 1);;\
readingsEndUpdate($hash, 1);;\
};;\
InternalTimer(gettimeofday() + 120, $timeoutFunction, $name.'Timeout');;\
\
if(!defined($buf)) {\
DevIo_CloseDev($hash);;\
#readingsBulkUpdate($hash, "last_seen", 0);;\
$buf = "not_connected";;\
}\
\
# only update our reading if buffer is not empty and looks like it contains a message\
if ($buf ne "" && \
$buf =~ /^{.*"event":"message".*}$/) { ## check if buffer looks like JSON with msg\
\
# delete all our readings that begin with "ntfy_"\
foreach my $reading (grep { $_ =~ /^ntfy_.*/ } keys %{$hash->{READINGS}}) {\
readingsDelete($hash, $reading);;\
}\
\
# parse as JSON, do not trust the input fully, thus sanitize buffer\
my %res = %{json2nameValue($buf)};; #(https://wiki.fhem.de/wiki/MQTT2_DEVICE_-_Schritt_f%C3%BCr_Schritt#json2nameValue.28.29)\
foreach my $k (sort keys %res) {\
# only keep ASCII and a German Characters like Umlaute, sharp-S...\
my $sanitizedValue = $res{$k} =~ s/[^[:ascii:]äöüÖÄÜß]/_/rg;; # 'r' flag prevents modifying the input string\
readingsBulkUpdate($hash, "ntfy_".makeReadingName($k), $sanitizedValue);;\
}\
}\
#readingsBulkUpdate($hash, "websocketData", "$buf") if ($buf ne "");;\
Log3($name, 3, "$name: Rx: >>>$buf<<<") if ($buf ne "");;\
\
readingsEndUpdate($hash, 1);;\
};;\
\
DevIo_OpenDev($hash,\
0, ## reopen flag\
undef, ## initFn, on success\
sub() { ## callbackFn, on verdict, req. to make it a non-blocking call\
my ($hash, $error) = @_;;\
if ($error) {\
Log(3, "$name: DevIo_OpenDev Callback: connection failed: $error");;\
\
my $timerFunction = sub() {\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $devState = DevIo_IsOpen($hash);;\
readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
};;\
\
RemoveInternalTimer($name.'Timer');;\
my $rwait = int(rand(20)) + 10;;\
InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.'Timer');;\
readingsSingleUpdate($hash, "cmd", "reconnect attempt in $rwait seconds", 1);;\
}\
}\
);;\
\
readingsBulkUpdate($hash, "state", "connecting...");;\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
disconnect:cmd:.(disconnect|reconnect) {\
my $hash = $defs{$name};;\
my $myCmd = ReadingsVal($name, "cmd", "???");;\
\
RemoveInternalTimer($name.'Timer');;\
RemoveInternalTimer($name.'Timeout');;\
DevIo_CloseDev($hash);;\
readingsBulkUpdate($hash, "state", "disconnected") if (!defined(DevIo_IsOpen($hash)));;\
\
if ($myCmd =~ /reconnect/) {\
my $timerFunction = sub() {\
my $hash = $defs{$name};;\
readingsSingleUpdate($hash, "cmd", "connect", 1);;\
};;\
\
RemoveInternalTimer("${name}_${reading}_timer");;\
InternalTimer(gettimeofday()+1, $timerFunction, "${name}_${reading}_timer");;\
} else {\
RemoveInternalTimer("${name}_watchdog_timer");;\
}\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
onDisconnect { ## check on each update if the connection is unintentionally broken...\
my $myState = ReadingsVal($name, "state", "???");;\
my $myData = ReadingsVal($name, "websocketData", "???");;\
my $myCmd = ReadingsVal($name, "cmd", "disconnect");;\
return if ($myState ne "disconnected" and $myData ne "not_connected");;\
return if ($myCmd =~ /disconnect|stop/);;\
\
my $timerFunction = sub() {\
my ($arg) = @_;;\
my $hash = $defs{$name};;\
my $devState = DevIo_IsOpen($hash);;\
readingsSingleUpdate($hash, "cmd", "connect", 1) if (!defined($devState));;\
};;\
\
RemoveInternalTimer($name.'Timer');;\
my $rwait = int(rand(20)) + 10;;\
InternalTimer(gettimeofday() + $rwait, $timerFunction, $name.'Timer');;\
readingsBulkUpdate($hash, "cmd", "reconnect attempt in $rwait seconds");;\
\
return POSIX::strftime("%H:%M:%S",localtime(time()));;\
},\
watchdog:last_seen:.* {\
my $ls = ReadingsVal($name, "last_seen", 0);;\
\
my $timerFunction = sub() {\
##fhem("set FHEMMeldung.ntfy message $name $reading wurde ausgelöst (last_seen: $ls)");;\
readingsSingleUpdate($hash, "cmd", "reconnect", 1);;\
readingsSingleUpdate($hash, "last_seen", 0, 1);;\
};;\
\
RemoveInternalTimer("${name}_${reading}_timer");;\
InternalTimer(gettimeofday()+240, $timerFunction, "${name}_${reading}_timer");;\
\
return POSIX::strftime("%H:%M:%S", localtime(time()));;\
}
attr NTFY_RECEIVE username Torxgewinde
attr NTFY_RECEIVE verbose 1
attr NTFY_RECEIVE webCmd start:stop