Ntfy

Aus FHEMWiki

📬 NTFY.sh: Push Nachrichten an iOS, Android, PC, Command-Line, E-Mail & Telefon

NTFY Screenshot mit einem Bild
NTFY Screenshot mit einem Bild

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

Wichtig: Nutzt man eine öffentliche Instanz ist das Topic so zu wählen, dass es wie ein Passwort funktioniert und schwer zu erraten ist. Wählt man ein einfaches Topic, können andere mitlesen und auch Nachrichten senden. Dies kann zu ungewollten Effekten führen!

NTFY mit anderen Systemen nutzen/verbinden

Auch ganz ohne FHEM, also aus anderen Systemen, kann man an NTFY Nachrichten senden und empfangen. So kann man sich aus iOS zum Beispiel per Webhook etwas an NTFY senden, oder auch von IP-Kameras Infos an den NTFY Server senden und dann von den diversen Clients empfangen und verarbeiten.

Apps (iOS, Android und PWA)

Ganz wichtig für die meisten Nutzer ist natürlich der Empfang von NTFY Nachrichten mit:

Hinweis zur nativen iOS-App: Zur Zeit funktioniert die Anzeige von Anhängen unter iOS, iPadOS, MacOS nur über die Web-App (zu finden unter https://ntfy.sh/app). Um die PWA unter iOS/MacOS fast wie eine gewöhnliche App zu nutzen, kann man die Web-Seite an den "Home-Screen" (iOS) senden oder "Zum Dock hinzufügen" (MacOS). Anschließend noch "Benachrichtigungen erlauben" aktivieren.

CLI, Commandline, aus Skripten heraus Nachrichten senden

Zum Beispiel kann man mit einem CURL-Befehl etwas senden:

curl -d "Your message content here" \
    -H "Title: Bla" \
    https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer

oder auch mit WGET:

wget --method=POST \
    --body-data="Your message content here" \
    --header="Title: Bla" \
    https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer -q -O -

Per Webhook Nachrichten senden

Via HTTP-GET als Webhook kann man ebenfalls Nachrichten via NTFY senden. Diese Option eignet sich besonders für weitere Geräte wie Shelly, Alexa oder auch Apple-Shortcuts und viele weitere Geräte dieser Art:

https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer/publish?message=Your%20message%20content%20here&title=Bla

Per E-Mail Nachrichten senden

Der NTFY Server kann auch per E-Mail erreicht werden, sofern dies so eingestellt wurde. Dies ist sehr praktisch, wenn ein System wie zum Beispiel eine IP-Kamera E-Mails bei Bewegung versenden kann, oder wenn ein CI-Build E-Mails verschickt. Die Doku dazu ist unter: https://docs.ntfy.sh/config/?h=mail#e-mail-publishing

Alternativ, kann man ein E-Mail Postfach überwachen und bei neuen E-Mails diese in eine NTFY Nachricht umwandeln und sich dann an die NTFY-Apps senden: 📧 Fetchmail, Email von IP-Cam ohne Verzögerung an NTFY/FHEM senden

Anbindung in FHEM per Modul/Device "NTFY_CLIENT"

Es gibt ein FHEM-Device, mit dem man NTFY nutzen kann: fhem-ntfy

Schritte um NTFY_CLIENT einzurichten:

Den Quelltext für das NTFY_CLIENT-Device kann man von Extern wie folgt in FHEM laden:

update add https://rm.byterazor.de/upd-fhem-ntfy/controls_byterazor-fhem-ntfy.txt
update

Nachdem man den Quelltext in FHEM hat, sollte FHEM einmal neugestartet werden:

shutdown restart

Danach kann man ein NTFY_CLIENT-Device anlegen:

defmod NTFY0 NTFY_CLIENT https://ntfy.sh
attr NTFY0 defaultPriority default
attr NTFY0 defaultTopic FreundlichenGruesseAnAlleFHEMNutzer
attr NTFY0 room Experimente

Senden von NTFY Nachrichten aus FHEM heraus:

Senden kann man Nachrichten mit:

set NTFY0 publish @FreundlichenGruesseAnAlleFHEMNutzer Testnachricht!

Die genaue Nutzung sind in dem Hilfetext zum Device zu finden. Das "@" setzt zum Beispiel das Topic für NTFY.

Empfangen und reagieren auf Nachrichten

Im Log wird es Meldungen in der folgenden Art geben wenn Nachrichten empfangen werden:

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"}

Hier ein Beispiel mit einem notify-Device, dass auf die Nachrichten "Garage auf" oder "Garage zu" reagieren soll:

defmod GarageNotify notify NTFY0:lastReceivedData:.* {\
	Log(1, "$NAME: $EVTPART1, $EVTPART2, $EVENT");;\
	\
	if ("$EVTPART1 $EVTPART2" eq "Garage zu") {\
		Log(1, "Garage soll zu gemacht werden");;\
		fhem("set NTFY0 publish Ich werde die Garage nun schliessen");;\
	}\
	\
	if ("$EVTPART1 $EVTPART2" eq "Garage auf") {\
		Log(1, "Garage soll auf gemacht werden");;\
		fhem("set NTFY0 publish Ich werde die Garage nun oeffnen");;\
	}\
}
attr GarageNotify room Experimente

Die zugehörigen Webhooks für obiges Beispiel-notify-Device wären dann:

Testen kann man direkt gegen die bekannteste und vom NTFY-Autor geschaffene NTFY-Installation: https://ntfy.sh/FreundlichenGruesseAnAlleFHEMNutzer

Anbindung per HTTPMOD und Websocket

Neben der Option per speziellem NTFY Device kann man auch das HTTPMOD-Device nutzen um Nachrichten oder Attachments/Dateien zu senden:

defmod NTFY HTTPMOD none 0
attr NTFY userattr Filename Markdown NtfyServer Priority Title Topic password username
attr NTFY Filename test.png
attr NTFY Markdown true
attr NTFY NtfyServer ntfy.sh
attr NTFY Priority high
attr NTFY Title Titel aus UserAttr heraus
attr NTFY Topic FreundlichenGruesseAnAlleFHEMNutzer
attr NTFY comment # for HTTP Basic authentication use this as "setUrl":\
# https://[$name:username]:[$name:password]@[$name:NtfyServer]/%%path%%
attr NTFY replacement01Mode text
attr NTFY replacement01Regex %%path%%
attr NTFY replacement02Mode expression
attr NTFY replacement02Regex \[([^:]+):([^\]]+)\]
attr NTFY replacement02Value my $device = $name if ($1 eq "\$name") // $1;;\
ReadingsVal($device, $2, undef) or AttrVal($device, $2, "???");;
attr NTFY replacement03Mode expression
attr NTFY replacement03Regex %%title%%
attr NTFY replacement04Mode expression
attr NTFY replacement04Regex %%message%%
attr NTFY replacement05Mode expression
attr NTFY replacement05Regex %%file%%
attr NTFY room Global
attr NTFY set1Data %%message%%
attr NTFY set1HeaderIcon Icon: https://fhem.de/www/images/default/fhemicon.png
attr NTFY set1HeaderMarkdown Markdown: [$name:Markdown]
attr NTFY set1HeaderPrio Priority: [$name:Priority]
attr NTFY set1HeaderTitle Title: %%title%%
attr NTFY set1Method POST
attr NTFY set1Name message
attr NTFY set1Replacement01Value [$name:Topic]
attr NTFY set1Replacement03Value # get the value as passed to the set command:\
my $value = InternalVal($name, "value", "???");;\
\
# find first occurence of pattern Title=".*" and use that as result:\
my ($result) = $value =~ /Title="(.*?)"/;;\
\
# assign value from userAttr if $result is emtpy:\
$result //= AttrVal($name, "Title", "???");;\
\
return $result;;
attr NTFY set1Replacement04Value # get the value as passed to the set command:\
my $value = InternalVal($name, "value", "???");;\
\
# remove everything matching pattern Title=".*"\
$value =~ s/Title=".*?"//;;\
\
#replace the literal character sequence\
# \n with a real linefeed\
$value =~ s/\\n/\n/g;;\
\
return $value;;
attr NTFY set1TextArg 1
attr NTFY set2Data %%file%%
attr NTFY set2HeaderFilename Filename: [$name:Filename]
attr NTFY set2HeaderPrio Priority: [$name:Priority]
attr NTFY set2HeaderTitle Title: [$name:Title]
attr NTFY set2Method PUT
attr NTFY set2Name attach
attr NTFY set2Replacement01Value [$name:Topic]
attr NTFY set2Replacement05Value my $value = InternalVal($name, "value", "???");;\
\
open(my $fh, '<', $value) or return "ERROR: Cannot open file";;\
binmode($fh);; \
my $result = do { local $/;; <$fh> };; \
close($fh);; \
\
return $result;;
attr NTFY set2TextArg 1
attr NTFY setURL https://[$name:NtfyServer]/%%path%%
attr NTFY widgetOverride Priority:select,max,high,default,low,min Markdown:select,true,false

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 👾
set NTFY attach /opt/fhem/www/images/default/fhemicon.png

Wenn man Dateien versendet, wird der Dateiname für den Empfänger per Reading oder Attribut definiert. Das Reading hat Vorrang:

setreading NTFY Filename meinDateiName.png
attr NTFY Filename meinDateiName.png

Empfangen mit Websocket und Dummydevice

Alternativ zum NTFY_CLIENT Modul kann man auch mit folgenden Snippet NTFY Nachrichten empfangen.

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

Weitere Links

Forumthread: 📬 NTFY.sh: Push Nachrichten an iOS, Android, PC, Command-Line, E-Mail & Telefon