Notdienst Apotheke via jsonmod

Begonnen von binford6000, 27 März 2023, 14:10:27

Vorheriges Thema - Nächstes Thema

binford6000

Hallo Zusammen,
da aponet die Auslieferung der Daten auf JSON umgestellt hat, habe ich ein kleines JSONMOD device erstellt welches die wesentlichen Daten aus dem riesigen JSON extrahiert und in leserliche Readings packt.

Prinzipiell funktioniert das auch noch weiterhin via HTTPMOD (siehe hier) - aber die Einrichtung bzw. Beschränkung auf die wesentlichen Readings ist aufwändiger und ich habe es auch nicht hinbekommen dass Umlaute aus dem JSON korrekt decodiert werden (zB. "Kleiststra�e 2").

defmod aponet.jmod JsonMod https://www.aponet.de/apotheke/notdienstsuche?tx_aponetpharmacy_search[action]=result&tx_aponetpharmacy_search[controller]=Search&tx_aponetpharmacy_search[search][plzort]=12345&tx_aponetpharmacy_search[search][date]=&tx_aponetpharmacy_search[search][street]=&tx_aponetpharmacy_search[search][radius]=2&tx_aponetpharmacy_search[search][lat]=&tx_aponetpharmacy_search[search][lng]=&type=1982
attr aponet.jmod event-on-change-reading .*
attr aponet.jmod icon message_medicine
attr aponet.jmod interval 33 8 * * *
attr aponet.jmod readingList single(jsonPath('results.apotheken.apotheke.0.distanz'),'distance','error');;\
single(jsonPath('results.apotheken.apotheke.0.enddatum'),'enddate','error');;\
single(jsonPath('results.apotheken.apotheke.0.endzeit'),'endtime','error');;\
single(jsonPath('results.apotheken.apotheke.0.id'),'id','error');;\
single(jsonPath('results.apotheken.apotheke.0.latitude'),'lat','error');;\
single(jsonPath('results.apotheken.apotheke.0.longitude'),'lon','error');;\
single(jsonPath('results.apotheken.apotheke.0.name'),'name','error');;\
single(jsonPath('results.apotheken.apotheke.0.ort'),'city','error');;\
single(jsonPath('results.apotheken.apotheke.0.plz'),'postcode','error');;\
single(jsonPath('results.apotheken.apotheke.0.startdatum'),'startdate','error');;\
single(jsonPath('results.apotheken.apotheke.0.startzeit'),'starttime','error');;\
single(jsonPath('results.apotheken.apotheke.0.strasse'),'street','error');;\
single(jsonPath('results.apotheken.apotheke.0.telefon'),'tel','error');;
attr aponet.jmod room 90_System->93_Datenquellen
attr aponet.jmod stateFormat name, tel, street in postcode city
attr aponet.jmod timestamp-on-change-reading .*
attr aponet.jmod userReadings map { my $ret = "https:\/\/www.openstreetmap.org\/?";;\
  $ret .= "mlat=".ReadingsVal($name,"lat","0");;\
  $ret .= "&mlon=".ReadingsVal($name,"lon","0");;\
  $ret .= "#map=18";;\
  $ret .= "\/".ReadingsVal($name,"lat","0");;\
  $ret .= "\/".ReadingsVal($name,"lon","0");;\
return $ret;;}

Ihr müsst lediglich eure PLZ im define ändern ([plzort]=12345) und den Radius anpassen ([radius]=2).

Wollt ihr eine Übersicht aller Readings bzw. mehr Apotheken sehen könnt ihr mit
attr <name> readingList complete();alle Readings aus dem JSON auslesen und an eure Bedürfnisse anpassen.

Falls die Daten bei euch an einem anderen Zeitpunkt aktualisiert werden sollten könnt ihr mit
attr <name> interval 33 8 * * *auch eine andere Uhrzeit eintragen. O.g. cron statement aktualisiert die Daten um 8:33 Uhr täglich.

Von user Wolle02 stammt das userReading für den OSM Link. Mehr dazu im original Thread. Thx!

VG Sebastian

betateilchen

#1
Ich verwende hier folgendes Attribut:

attr apo readingList multi(jsonPath("\$.results.apotheken.apotheke.[?(\@.kammer in ['aknds'])]"), property('ort'), concat(property('name'),', ',property('strasse')));;

Hintergrund: Die Suche liefert Apotheken, die per Luftlinie in einem vorgegebenen Radius liegen, zu denen ich aber 70km fahren müsste, weil sie auf der anderen Seite der Elbe liegen.

Du darfst diesen Dateianhang nicht ansehen.

Deshalb wird die Ergebnisliste auf die Apotheken der niedersächsischen Apothekenkammer ausgewertet und nur solche Apotheken in die readings übernommen, die südlich der Elbe (in Niedersachsen) sind.

setstate apo 2023-03-27 16:10:39 .computedReadings Harsefeld,Stade,Buxtehude
setstate apo 2023-03-27 16:10:39 Buxtehude Kloster-Apotheke, Stader Str. 17
setstate apo 2023-03-27 16:10:39 Harsefeld Alte Apotheke, Herrenstr. 1
setstate apo 2023-03-27 16:10:39 Stade Fontane-Apotheke, Stockhausstr. 1 a
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

binford6000

Die Kammer hatte ich für mich entsorgt da ich für den Notdienst nicht in ein anderes Bundesland muss.
Für meine Definition oben ergänzend:
single(jsonPath('results.apotheken.apotheke.0.kammer'),'kammer','error');


betateilchen

Nochmal zum Verständnis: Die Kammer brauche ich nicht als reading, sondern als Suchkriterium. Da man sie offenbar nicht in der Suche direkt als Kriterium angeben kann, habe ich sie als Selektionskriterium für die Erzeugung von readings benutzt. In der JSON response steckt die Kammer ja in jeder Apotheke drin.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

juergen012

SUPER Vielen Dank für das Modul!!
Fhem unter Proxmox

betateilchen

Das ist kein Modul, sondern maximal ein device.
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

betateilchen

Zitat von: betateilchen am 27 März 2023, 16:42:44Nochmal zum Verständnis: Die Kammer brauche ich nicht als reading, sondern als Suchkriterium. Da man sie offenbar nicht in der Suche direkt als Kriterium angeben kann, habe ich sie als Selektionskriterium für die Erzeugung von readings benutzt. In der JSON response steckt die Kammer ja in jeder Apotheke drin.

Bei mir kommen aktuell keine Daten mehr von der angegebenen URL.

{"alerts":[],"settings":{"interface":"emergency","singleViewUid":"261"},"args":{"action":"result","controller":"Search","search":{"plzort":"21635","radius":5}},"results":[]}
-----------------------
Formuliere die Aufgabe möglichst einfach und
setze die Lösung richtig um - dann wird es auch funktionieren.
-----------------------
Lesen gefährdet die Unwissenheit!

yersinia

#7
Ich denke, man möchte nicht, dass irgendwer die Daten hier schmarotzenderweise abzieht - man möge gefälligst ein Widget nutzen oder ein individuelles Angebot erbetteln ([1] [2]).

In der Anfrage muss nun mittlerweile auch ein Token mitgegeben werden, ich hab es mit diesem reproduzieren können:
&tx_aponetpharmacy_search[token]=H8aumzJ76JQDer Vollständigkeithalber der JsonMod link aus #1 wäre dann:
https://www.aponet.de/apotheke/notdienstsuche?tx_aponetpharmacy_search[action]=result&tx_aponetpharmacy_search[controller]=Search&tx_aponetpharmacy_search[search][plzort]=12345&tx_aponetpharmacy_search[search][date]=&tx_aponetpharmacy_search[search][street]=&tx_aponetpharmacy_search[search][radius]=2&tx_aponetpharmacy_search[search][lat]=&tx_aponetpharmacy_search[search][lng]=&tx_aponetpharmacy_search[token]=H8aumzJ76JQ&type=1982Btw, anstelle der PLZ nutze ich lang&lat, das ist etwas genauer und funktioniert ganz gut. :)

Es gibt keine Garantie, dass dies dauerhaft funktioniert; es würde mich nicht wundern, wenn der Token irgendwann abläuft.

EDIT
Anbei mein readingsList inklusive zweier Definitionen für OSM Position und Route:
attr NotApo readingList multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_name"), property('.name'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_street"), property('.strasse'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_zip"), property('.plz'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_city"), property('.ort'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_tel"), property('.telefon'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_fax"), property('.fax'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_email"), property('.email'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_distance"), property('.distanz'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_longitude"), property('.longitude'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_latitude"), property('.latitude'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_start_date"), property('.startdatum'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_start_time"), property('.startzeit'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_end_date"), property('.enddatum'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_end_time"), property('.endzeit'));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_map_position"), concat("https:\/\/www.openstreetmap.org\/?mlat=", property('.latitude'), "&mlon=", property('.longitude'), "#map=18\/", property('.latitude'), "\/", property('.longitude')));;\
multi(jsonPath("\$.results.apotheken.apotheke.[*]"), concat(count(),"_map_route"), concat("https:\/\/www.openstreetmap.org\/directions?engine=", AttrVal($name,"osm_engine","fossgis_osrm_car"), "&route=", AttrVal("global","latitude","0"), "%2C", AttrVal("global","longitude","0"), "%3B" , property('.latitude'), "%2C", property('.longitude')));;
Für die Route werden die longitude und latitude Attribute aus dem global Device als Startpunkt genommen. Die Engine ist auswählbar via
attr NotApo userattr osm_engine:graphhopper_car,fossgis_osrm_car,fossgis_valhalla_car,graphhopper_bicycle,fossgis_osrm_bike,fossgis_valhalla_bicycle,graphhopper_foot,fossgis_osrm_foot,fossgis_valhalla_foot
attr NotApo osm_engine fossgis_osrm_car
Weiterhin ein alternatives stateFormat welches die ersten vier Apotheken anzeigt (inkl Links zur Position, Route, Telefonnummer und email):
attr NotApo stateFormat {    my $ret .= "<div style=\"display:table;;width:100%;;\">";; #table\
    for(my $i = 0;;$i <= 3;;$i++) {\
        $ret .= "<div style=\"display:table-row;;width:100%;;\">";; #row\
            $ret .= "<div style=\"display:table-cell;;padding:3pt;;width:50%;;text-align:left;;\">";; #cell\
                $ret .= "<a href=\"".ReadingsVal($name,$i."_map_position","https:\/\/www.osm.org");;\
                $ret .= "\" rel=\"noopener noreferrer\" target=\"_blank\" style=\"font-weight:bold;;\">".ReadingsVal($name,$i."_name","?")."</a>";;\
                $ret .= " (".sprintf("%.1f", ReadingsNum($name,$i."_distance",0))."km)<br \/>";;\
                $ret .= "<a href=\"".ReadingsVal($name,$i."_map_route","https:\/\/www.osm.org")."\" rel=\"noopener noreferrer\" target=\"_blank\">";;\
                $ret .= ReadingsVal($name,$i."_street","")."<br /\>";;\
                $ret .= ReadingsVal($name,$i."_zip","")."&nbsp;;".ReadingsVal($name,$i."_city","");;\
                $ret .= "</a>";;\
            $ret .= "</div>";; #/cell\
            $ret .= "<div style=\"display:table-cell;;width:50%;;text-align:left;;\">";; #cell\
                my $numberlink = ReadingsVal($name,$i."_tel","");;\
                $numberlink =~ s/\s+//g;;\
                $ret .= "Tel.: <a href=\"tel:+49".substr($numberlink, 1)."\">".ReadingsVal($name,$i."_tel","")."</a><br />";;\
                $ret .= "Fax: ".ReadingsVal($name,$i."_fax","")."<br />";;\
                $ret .= "eMail: <a href=\"mailto:".ReadingsVal($name,$i."_email","")."\">".ReadingsVal($name,$i."_email","")."</a>";;\
            $ret .= "</div>";; #/cell\
        $ret .= "</div>";; #/row\
        $ret .= "<div style=\"display:table-row;;width:100%;;\">";; #row\
            $ret .= "<div style=\"display:table-cell;;padding:3pt;;width:50%;;text-align:left;;\">";; #cell\
                $ret .= "Von ".ReadingsVal($name,$i."_start_date","")." ".ReadingsVal($name,$i."_start_time","");;\
            $ret .= "</div>";; #/cell\
            $ret .= "<div style=\"display:table-cell;;width:50%;;text-align:left;;\">";; #cell\
                $ret .= "Bis ".ReadingsVal($name,$i."_end_date","")." ".ReadingsVal($name,$i."_end_time","");;\
            $ret .= "</div>";; #/cell\
        $ret .= "</div>";; #/row\
    }\
    $ret .= "</div>";; #/table\
    return $ret;;\
}

Und weil ich es praktisch finde:
attr NotApo webCmd reread
viele Grüße, yersinia
----
FHEM 6.3 (SVN) on RPi 4B with RasPi OS Bullseye (perl 5.32.1) | FTUI
nanoCUL->2x868(1x ser2net)@tsculfw, 1x433@Sduino | MQTT2 | Tasmota | ESPEasy
VCCU->14xSEC-SCo, 7xCC-RT-DN, 5xLC-Bl1PBU-FM, 3xTC-IT-WM-W-EU, 1xPB-2-WM55, 1xLC-Sw1PBU-FM, 1xES-PMSw1-Pl

binford6000

ZitatEs gibt keine Garantie, dass dies dauerhaft funktioniert; es würde mich nicht wundern, wenn der Token irgendwann abläuft.
Scheint seit gestern der Fall zu sein...  :o

Christoph Morrison

Das neue Token ist pFCmJ+shPJg so wie es aussieht. Eine langfristige Lösung wird aber auch dieses nicht sein.

binford6000

Zitatso wie es aussieht

Sicher? Bei mir funktioniert o.g. Token auch nicht.
Wo kann ich diesen denn auslesen?

yersinia

Soweit ich, als Laie(!), das beurteilen kann, scheint es ein Server-seitiger Token zu sein; ich hab noch
fT9xDdr9vN0
q1KqFuY0jhk
gefunden.

Gefunden habe ich das über die Webentwicklerwerkzeuge des FF und dann die Netzwerkanalyse. Siehe:
Du darfst diesen Dateianhang nicht ansehen.

Aber mir ist es noch nicht gelungen herauszufinden, wie die POST Anfrage gestellt wird. Im Quelltext findet man zwar das Formular mit ein paar hidden input fields
<form role="search" novalidate="novalidate" name="search" id="pharmacy-searchform" action="/apotheke/notdienstsuche?tx_aponetpharmacy_search%5Baction%5D=search&amp;tx_aponetpharmacy_search%5Bcontroller%5D=Search&amp;cHash=d60644fbe4920abed16b25ce29f3b7c8" method="post">
[...]
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][@extension]" value="AponetPharmacy">
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][@vendor]" value="Ahlene">
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][@controller]" value="Search">
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][@action]" value="search">
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][arguments]" value="YTozOntzOjY6ImFjdGlvbiI7czo2OiJzZWFyY2giO3M6MTA6ImNvbnRyb2xsZXIiO3M6NjoiU2VhcmNoIjtzOjY6InNlYXJjaCI7YTozOntzOjY6InBsem9ydCI7czo1OiI5MDc2OCI7czo2OiJyYWRpdXMiO3M6MToiNSI7czo2OiJzdHJlZXQiO3M6MToiICI7fX0=df8ac474fdf38a63d082716d7b7fb3d14651a2c6">
<input type="hidden" name="tx_aponetpharmacy_search[__referrer][@request]" value="a:4:{s:10:&quot;@extension&quot;;s:14:&quot;AponetPharmacy&quot;;s:11:&quot;@controller&quot;;s:6:&quot;Search&quot;;s:7:&quot;@action&quot;;s:6:&quot;search&quot;;s:7:&quot;@vendor&quot;;s:6:&quot;Ahlene&quot;;}324369933c9c7bc5c9b00e3068fc6867610f9be7"> <input type="hidden" name="tx_aponetpharmacy_search[__trustedProperties]" value="a:1:{s:6:&quot;search&quot;;a:4:{s:6:&quot;plzort&quot;;i:1;s:4:&quot;date&quot;;i:1;s:6:&quot;street&quot;;i:1;s:6:&quot;radius&quot;;i:1;}}ee7948283ecf18e25699d9819a79aec0c7c553dd">
aber um von hier aus weiter analysieren zu können fehlen mir die Möglichkeiten. :(
viele Grüße, yersinia
----
FHEM 6.3 (SVN) on RPi 4B with RasPi OS Bullseye (perl 5.32.1) | FTUI
nanoCUL->2x868(1x ser2net)@tsculfw, 1x433@Sduino | MQTT2 | Tasmota | ESPEasy
VCCU->14xSEC-SCo, 7xCC-RT-DN, 5xLC-Bl1PBU-FM, 3xTC-IT-WM-W-EU, 1xPB-2-WM55, 1xLC-Sw1PBU-FM, 1xES-PMSw1-Pl

Christoph Morrison

Ich vermute, dass das Token dynamisch serverseitig generiert wird, quasi ein CSRF-Token. Damit kann man dann, wie wir hier sehen, erfolgreich Anfragen abwehren, die nicht von der Suchmaske an sich kommen.

yersinia

Zitat von: Christoph Morrison am 02 Mai 2023, 09:27:04Damit kann man dann, wie wir hier sehen, erfolgreich Anfragen abwehren, die nicht von der Suchmaske an sich kommen.
Wo wir wieder bei
Zitat von: yersinia am 28 April 2023, 08:49:03Ich denke, man möchte nicht, dass irgendwer die Daten hier schmarotzenderweise abzieht - man möge gefälligst ein Widget nutzen oder ein individuelles Angebot erbetteln ([1] [2]).
wären. ::)

Wie kann es nun weiter gehen? Können wir diesen Token FHEMseitig vom Server abfragen bzw. eine 'richtige' Anfrage simulieren?
(davon abgesehen könnte man aponet auch freundlich anfragen ob man für FHEM einen eigenen persistenten Token bekommt)

Alternativ: kann HTTPMOD POST senden und empfangenes GET verarbeiten? Ich denke nicht, dass wir mit der derzeitigen JsonMod Version hier weiterkommen werden.
viele Grüße, yersinia
----
FHEM 6.3 (SVN) on RPi 4B with RasPi OS Bullseye (perl 5.32.1) | FTUI
nanoCUL->2x868(1x ser2net)@tsculfw, 1x433@Sduino | MQTT2 | Tasmota | ESPEasy
VCCU->14xSEC-SCo, 7xCC-RT-DN, 5xLC-Bl1PBU-FM, 3xTC-IT-WM-W-EU, 1xPB-2-WM55, 1xLC-Sw1PBU-FM, 1xES-PMSw1-Pl

binford6000

Zitatdavon abgesehen könnte man aponet auch freundlich anfragen ob man für FHEM einen eigenen persistenten Token bekommt)
Ich habe mal eine nette Mail an aponet-download_at_avoxa.de geschrieben und unser Token-Dilemma beschrieben.
Mal gespannt ob und was da zurück kommt...