Zitat von: Hadl am 20 Februar 2026, 22:51:59Stimmt, aber im Notify auf "Error" weis ich den Befehl ja leider nichtmehr der fehlgeschlagen ist. Ich müsste erst einen Soll/Ist vergleich machen um den Befehl nochmals zu senden. Das ist relativ hoher Programieraufwand und ich wollte schauen ob es nicht einfacher geht.Du kannst bei Netzwerkproblemen das Attribut timeout auf einen höheren Wert (Standard: 4 sec) setzen. Es werden dann zusätzliche Readings mit den aktuellen/maximalen Reaktionszeiten und ein Reading mit dem letzten SET Befehl angelegt.
Zitat von: locodriver am 20 Februar 2026, 17:47:09Zitat von: CoolTux am 27 Januar 2026, 19:02:05appId":"de.zdf.app.zdfm3
Das ist genau das was ich brauche. Danke Dir Uwe. Ich schau mal das ich die Tage die Anpassungen mache.
Hallo CoolTux, hast du schon Zeit gefunden,die Ergänzung einzubauen?
Dankeschön.
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Getopt::Long qw(GetOptions);
use LWP::UserAgent;
use URI;
use XML::LibXML;
use XML::Writer;
use IO::File;
# ---------------- CLI ----------------
my $location = undef; # Pflicht, z.B. http://192.168.0.254:49000/MediaServerDevDesc.xml
my $out_file = "presets.xml";
my $start_id = 1;
my $max_presets = 99;
my $timeout_s = 12;
my $debug = 0;
GetOptions(
"location=s" => \$location,
"out=s" => \$out_file,
"start=i" => \$start_id,
"max=i" => \$max_presets,
"timeout=i" => \$timeout_s,
"debug!" => \$debug,
) or die "Usage: $0 --location <URL> [--out presets.xml] [--start 1] [--max 99] [--timeout 12] [--debug]\n";
die "Fehlt: --location (z.B. http://192.168.0.254:49000/MediaServerDevDesc.xml)\n"
if !$location;
# ---------------- Helpers ----------------
sub norm { my $s = shift // ""; $s =~ s/^\s+|\s+$//g; return $s; }
sub lc_norm { return lc(norm(shift)); }
# ---------------- HTTP Client ----------------
my $ua = LWP::UserAgent->new(
agent => "fritz-favs-to-presets/1.0",
timeout => $timeout_s,
);
$ua->env_proxy;
# ---------------- 1) Device Description holen ----------------
my $desc_res = $ua->get($location);
die "GET DeviceDesc fehlgeschlagen: " . $desc_res->status_line . "\n"
if !$desc_res->is_success;
my $desc_xml = $desc_res->decoded_content;
my $desc_doc = XML::LibXML->load_xml(string => $desc_xml);
# Base-URL für relative controlURL
my $loc_uri = URI->new($location);
my $base = $loc_uri->scheme . "://" . $loc_uri->host_port;
# UDN (ohne uuid:)
my ($udn_node) = $desc_doc->findnodes('//*[local-name()="UDN"]');
my $udn = $udn_node ? norm($udn_node->textContent) : "";
$udn =~ s/^uuid://i;
die "Konnte UDN nicht aus DeviceDesc lesen.\n" if !$udn;
# ContentDirectory Service finden -> controlURL
my ($cd_service) = $desc_doc->findnodes(
'//*[local-name()="service"]/*[local-name()="serviceType" and contains(., "ContentDirectory:1")]/..'
);
die "Kein ContentDirectory:1 Service in DeviceDesc gefunden.\n" if !$cd_service;
my ($control_node) = $cd_service->findnodes('./*[local-name()="controlURL"]');
die "Kein controlURL im ContentDirectory Service.\n" if !$control_node;
my $control_url = norm($control_node->textContent);
$control_url = $base . $control_url if $control_url =~ m{^/};
print "Using FRITZ!Box ContentDirectory controlURL: $control_url\n" if $debug;
print "sourceAccount: $udn/0\n" if $debug;
# ---------------- 2) SOAP Browse ----------------
sub soap_browse {
my (%p) = @_;
my $object_id = $p{ObjectID} // "0";
my $flag = $p{BrowseFlag} // "BrowseDirectChildren";
my $body =
qq{<?xml version="1.0" encoding="utf-8"?>\n}
. qq{<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">\n}
. qq{ <s:Body>\n}
. qq{ <u:Browse xmlns:u="urn:schemas-upnp-org:service:ContentDirectory:1">\n}
. qq{ <ObjectID>$object_id</ObjectID>\n}
. qq{ <BrowseFlag>$flag</BrowseFlag>\n}
. qq{ <Filter>*</Filter>\n}
. qq{ <StartingIndex>0</StartingIndex>\n}
. qq{ <RequestedCount>0</RequestedCount>\n}
. qq{ <SortCriteria></SortCriteria>\n}
. qq{ </u:Browse>\n}
. qq{ </s:Body>\n}
. qq{</s:Envelope>\n};
my $req = HTTP::Request->new(POST => $control_url);
$req->header('Content-Type' => 'text/xml; charset="utf-8"');
$req->header('SOAPACTION' => '"urn:schemas-upnp-org:service:ContentDirectory:1#Browse"');
$req->content($body);
my $res = $ua->request($req);
if (!$res->is_success) {
die "SOAP Browse fehlgeschlagen (ObjectID=$object_id): " . $res->status_line . "\n"
. $res->decoded_content . "\n";
}
my $soap = XML::LibXML->load_xml(string => $res->decoded_content);
# <Result> enthält DIDL-Lite als escaped XML
my ($result_node) = $soap->findnodes('//*[local-name()="Result"]');
my $result = $result_node ? $result_node->textContent : "";
$result = norm($result);
return $result;
}
# ---------------- 3) DIDL-Lite parsen ----------------
sub parse_didl {
my ($didl_str) = @_;
return ([], []) if !$didl_str;
my $didl_doc = XML::LibXML->load_xml(string => $didl_str);
my @containers;
for my $c ($didl_doc->findnodes('//*[local-name()="container"]')) {
my $id = $c->getAttribute("id") // "";
my ($t) = $c->findnodes('./*[local-name()="title"]');
my $title = $t ? norm($t->textContent) : "";
push @containers, { id => norm($id), title => $title };
}
my @items;
for my $it ($didl_doc->findnodes('//*[local-name()="item"]')) {
my $id = $it->getAttribute("id") // "";
my ($t) = $it->findnodes('./*[local-name()="title"]');
my $title = $t ? norm($t->textContent) : "";
my ($art) = $it->findnodes('.//*[local-name()="albumArtURI"]');
my $albumart = $art ? norm($art->textContent) : "";
push @items, { id => norm($id), title => $title, art => $albumart };
}
return (\@containers, \@items);
}
# ---------------- 4) Debug: Container/Items auflisten ----------------
sub dump_children_one_level {
my ($parent_id, $limit_items) = @_;
$limit_items //= 15;
my $didl = soap_browse(ObjectID => $parent_id, BrowseFlag => "BrowseDirectChildren");
my ($containers, $items) = parse_didl($didl);
print "\n== Children of $parent_id ==\n";
print "Containers: " . scalar(@$containers) . "\n";
for my $c (@$containers) {
printf " [C] %-45s id=%s\n", ($c->{title} // ""), ($c->{id} // "");
}
print "Items: " . scalar(@$items) . "\n";
my $max = @$items < $limit_items ? @$items : $limit_items;
for (my $i=0; $i<$max; $i++) {
my $it = $items->[$i];
printf " [I] %-45s id=%s\n", ($it->{title} // ""), ($it->{id} // "");
}
}
# ---------------- 5) BFS: Container per Titel finden ----------------
sub find_container_bfs {
my (%p) = @_;
my $root = $p{root} // "0";
my $want_re = $p{want_re}; # regex auf lowercased title
my $max_nodes = $p{max_nodes} // 12000;
my @q = ($root);
my %seen;
my $visited = 0;
while (@q) {
my $cur = shift @q;
next if $seen{$cur}++;
last if ++$visited > $max_nodes;
my $didl = soap_browse(ObjectID => $cur, BrowseFlag => "BrowseDirectChildren");
my ($containers, undef) = parse_didl($didl);
for my $c (@$containers) {
my $t = lc_norm($c->{title});
return $c->{id} if defined($want_re) && $t =~ $want_re;
push @q, $c->{id};
}
}
return undef;
}
# ---------------- 6) Fallback: "beste Senderliste" finden ----------------
# Heuristik: Container mit vielen Items und wenigen Subcontainern
sub find_best_station_list_container {
my (%p) = @_;
my $root = $p{root} // "0";
my $max_nodes = $p{max_nodes} // 20000;
my @q = ($root);
my %seen;
my $visited = 0;
my $best_id = undef;
my $best_score = -1;
my $best_items = 0;
my $best_conts = 0;
while (@q) {
my $cur = shift @q;
next if $seen{$cur}++;
last if ++$visited > $max_nodes;
my $didl = soap_browse(ObjectID => $cur, BrowseFlag => "BrowseDirectChildren");
my ($containers, $items) = parse_didl($didl);
my $num_items = scalar(@$items);
my $num_conts = scalar(@$containers);
# Favoritenlisten sind oft: viele Items, wenig Untercontainer
my $score = $num_items * 10 - $num_conts;
if ($num_items > 0 && $score > $best_score) {
$best_score = $score;
$best_id = $cur;
$best_items = $num_items;
$best_conts = $num_conts;
}
# weiter runter
for my $c (@$containers) {
push @q, $c->{id};
}
}
print "Fallback best container: $best_id (items=$best_items containers=$best_conts score=$best_score)\n"
if $debug && defined $best_id;
return $best_id;
}
# ---------------- 7) MAIN: Radio -> Favoriten -> Items ----------------
# 7.1: Radio/Internetradio Container finden (global)
my $radio_id = find_container_bfs(
root => "0",
want_re => qr/(internet\s*radio|internetradio|radio)/,
max_nodes => 20000
) or die "Konnte keinen Container 'Internetradio/Radio' finden.\n";
print "Radio container found: $radio_id\n" if $debug;
dump_children_one_level($radio_id) if $debug;
# 7.2: Favoriten darunter finden (per Titel), sonst Fallback (beste Liste)
my $fav_id = find_container_bfs(
root => $radio_id,
want_re => qr/(favorit|favorite|lieblings|favourites)/,
max_nodes => 20000
);
if (!$fav_id) {
print "Kein Favoriten-Container per Titel gefunden – nutze Fallback (Container mit den meisten Sender-Items)...\n"
if $debug;
$fav_id = find_best_station_list_container(root => $radio_id, max_nodes => 30000);
}
die "Konnte keine Senderliste unterhalb von '$radio_id' finden.\n" if !$fav_id;
print "Station list container: $fav_id\n" if $debug;
dump_children_one_level($fav_id) if $debug;
# 7.3: Items (Sender) direkt aus Favoritencontainer lesen
my $fav_didl = soap_browse(ObjectID => $fav_id, BrowseFlag => "BrowseDirectChildren");
my (undef, $items) = parse_didl($fav_didl);
die "Keine Sender-Items im Container ($fav_id) gefunden.\n" if !@$items;
# ---------------- 8) presets.xml schreiben ----------------
my $fh = IO::File->new(">$out_file") or die "Kann $out_file nicht schreiben: $!\n";
binmode($fh, ":utf8");
my $w = XML::Writer->new(OUTPUT => $fh, DATA_MODE => 1, DATA_INDENT => 2, ENCODING => "UTF-8");
my $now = time();
$w->xmlDecl("UTF-8");
$w->startTag("presets");
my $pid = int($start_id);
my $count = 0;
for my $st (@$items) {
last if $count >= $max_presets;
my $name = $st->{title} || "Unbenannt";
my $oid = $st->{id};
next if !$oid;
$w->startTag("preset", id => $pid, createdOn => $now, updatedOn => $now);
$w->startTag("ContentItem",
source => "STORED_MUSIC",
location => $oid,
sourceAccount => $udn . "/0",
isPresetable => "true",
);
$w->dataElement("itemName", $name);
$w->dataElement("containerArt", $st->{art}) if $st->{art};
$w->endTag("ContentItem");
$w->endTag("preset");
$pid++;
$count++;
}
$w->endTag("presets");
$w->end();
$fh->close();
print "OK: $count Sender nach $out_file geschrieben.\n";
exit 0;
Zitat von: JoWiemann am 20 Februar 2026, 18:47:11welche Variable soll das denn sein?Ihr hattet doch etwas geschrieben von $returnListMediaServers, oder $info. Beides wurde im BOSEST-Modul in keiner Weise angerührt, das Modul im FHEM-repository seit ziemlich langer Zeit überhaupt nicht verändert.
Zitat von: JoWiemann am 20 Februar 2026, 18:51:05dass würde mich interessieren. Ggf. kann ich das ja in das FritzBox Modul übernehmen.Gerne doch => https://forum.fhem.de/index.php?topic=143976.0
Zitat von: JoWiemann am 20 Februar 2026, 18:05:35Was ich gefunden habe ist, im 98_BOSEST.pm gibt es $info->{info}Sorry, das ist eine _lokale_ Variable, die kann mit gar nichts kollidieren.
(set MQTT publish pixel[1-3]/custom/TMP {"icon":"tmp_ani_out_01","text":"[TMP]"})
Zitat von: Starkstrombastler am 15 Februar 2026, 10:55:16Wenn der Befehl von Fhem an den Shelly nicht erfolgreich war, wird der State im Shelly-Device auf Error gesetzt. Dies kann durch ein Notify abgefragt werden um ein erneutes Senden des Befehls auszulösen.Stimmt, aber im Notify auf "Error" weis ich den Befehl ja leider nichtmehr der fehlgeschlagen ist. Ich müsste erst einen Soll/Ist vergleich machen um den Befehl nochmals zu senden. Das ist relativ hoher Programieraufwand und ich wollte schauen ob es nicht einfacher geht.
Zitat von: Starkstrombastler am 15 Februar 2026, 10:55:16Da aber das Heizen mit einem Heizstab durchaus sicherheitsrelevant ist, sollte hier aber die Strategie geändert werden: statt mehrfach Ausschaltbefehle zu senden (in der Hoffnung, dass dies irgendwann funktioniert) sollte besser das Einschalten auf Gerätebasis zeitlich begrenzt werden und bedarfsweise nachgetriggert werden.Ja, Sicherheit ist hier wirklich wichtig. Mein Heizstab hat eine Sicherheitsabschaltung mit separaten Relais bei Übertemperatur, eine "normale" Abschaltung per Thermostat einstellbarer Temperatur mit Relais. Und eben nun auch noch die Schaltung durch den Shelly über fhem Logik. Wenn das Ausschalten nicht klappt heizt mein Warmwasser dauerhaft immer auf ca. 60°C
Zitat von: Starkstrombastler am 15 Februar 2026, 10:55:16Für das zeitliche Begrenzen bietet sich auf Shelly-Modul-Basis der Befehl "on-for-timer" an. Dieser setzt den Shelly-internen Timer und kann mit neuem "on-for-timer" nachgetriggert werden. Ein eventuell auf dem Shelly eingerichteter Ausschalttimer wird dabei ignoriert. Der Shelly schaltet dann bei Ablauf der Zeitspanne aus - unabhängig von Fhem und Netzwerk.Ja, damit krieg ich auf jeden Fall einen "timeout" beim Abschalten hin, muss dann nur schauen das ich den Befehl oft genug wiederhole. Aktuell sende ich mit einen DOIF nur Befehle bei einer Änderung direkt aus dem DOIF an das Shelly Device.
Zitat von: jkriegl am 15 Februar 2026, 19:01:00Hat der Shelly eine feste IP-Adresse?Ja, hat er.
Hatte ein vergleichbares Online-Problem mit Schalten einer Zirkulationspumpe. Seit 3 Mon. nicht mehr aufgetreten.
