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
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
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 kann man dann ganz einfach 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.
set NTFY0 publish @FreundlichenGruesseAnAlleFHEMNutzer Testnachricht!
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"}
Testen kannst 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