Hauptmenü

Neueste Beiträge

#91
Codeschnipsel / Aw: Abfrage der Radiosender ei...
Letzter Beitrag von Prof. Dr. Peter Henning - 23 Februar 2026, 15:26:06
Ich habe den Code jetzt noch einmal geändert. Für jeden auf der FB gefundenen Stream wird abgefragt, welche Kanalnummer auf einer Bose-Box man haben möchte (1..20). Wenn diese 1..6 beträgt, wird eine xml-Datei für das Senden an die Box erstellt, wenn diese >6 ist, wird eine Attributdefinition für das BOSEST-Modul geschrieben.

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use open ':std', ':encoding(UTF-8)';

use Getopt::Long qw(GetOptions);
use LWP::UserAgent;
use HTTP::Request::Common qw(POST GET);
use URI;
use XML::LibXML;

# ------------------------------------------------------------
# CLI options
# ------------------------------------------------------------
my $desc_url = '';
my $host     = '';
my $port     = 49000;
my $timeout  = 10;
my $maxdepth = 50;
my $debug    = 0;

GetOptions(
  'desc=s'     => \$desc_url,   # full URL to MediaServerDevDesc.xml
  'host=s'     => \$host,       # host/IP of fritzbox
  'port=i'     => \$port,
  'timeout=i'  => \$timeout,
  'maxdepth=i' => \$maxdepth,
  'debug!'     => \$debug,
) or die usage();

if (!$desc_url) {
  $host or die usage();
  $desc_url = "http://$host:$port/MediaServerDevDesc.xml";
}

my $ua = LWP::UserAgent->new(
  timeout => $timeout,
  agent   => "fritz_interactive_channels_and_presets/1.0",
);

# ------------------------------------------------------------
# Discover ContentDirectory + UDN
# ------------------------------------------------------------
my ($base_url, $control_url, $service_type, $udn) = get_contentdirectory_control_and_udn($ua, $desc_url);

my $source_account = normalize_udn($udn) . "/0";

print "Device description: $desc_url\n" if $debug;
print "Base URL:          $base_url\n"   if $debug;
print "Control URL:       $control_url\n"if $debug;
print "ServiceType:       $service_type\n"if $debug;
print "UDN raw:           $udn\n"        if $debug;
print "sourceAccount:     $source_account\n" if $debug;

# ------------------------------------------------------------
# Ask FHEM device name (needed for boseattr.txt output)
# ------------------------------------------------------------
print "\nFHEM Device-Name für boseattr.txt (z.B. WohnzimmerBose): ";
chomp(my $fhem_dev = <STDIN>);
$fhem_dev ||= "BoseDevice";

# Output files
my $file_presets = "payload_storepreset.xml";
my $file_attrs   = "boseattr.txt";

open my $fh_preset, ">:encoding(UTF-8)", $file_presets
  or die "Cannot write $file_presets: $!\n";
open my $fh_attr, ">:encoding(UTF-8)", $file_attrs
  or die "Cannot write $file_attrs: $!\n";

print "\nSchreibe Preset-Payload nach: $file_presets\n";
print "Schreibe FHEM attrs nach:     $file_attrs\n";
print "\nEingabe pro Item: 1..6 = Preset-ID, >=10 = channel_xx, n = skip, q = quit\n\n";

# Keep track of assigned presets 1..6
my %preset_used;

my %seen_containers;
my $quit = 0;

interactive_crawl(
  ua            => $ua,
  base_url      => $base_url,
  control_url   => $control_url,
  service_type  => $service_type,
  container_id  => '0',
  depth         => 0,
  maxdepth      => $maxdepth,
  seen          => \%seen_containers,
  debug         => $debug,
  quit_ref      => \$quit,
  fh_preset     => $fh_preset,
  fh_attr       => $fh_attr,
  fhem_dev      => $fhem_dev,
  source_account=> $source_account,
  preset_used   => \%preset_used,
);

close $fh_preset;
close $fh_attr;

print "\nFertig.\n";
print " - $file_presets (Preset-Payload, max 6 Einträge)\n";
print " - $file_attrs   (FHEM attr Zeilen)\n";
exit 0;

# ============================================================
# Helpers
# ============================================================

sub usage {
  return <<"TXT";
Usage:
  $0 --host <ip|fritz.box> [--port 49000] [--timeout 10] [--maxdepth 50] [--debug]
  $0 --desc <url_to_MediaServerDevDesc.xml> [--timeout 10] [--maxdepth 50] [--debug]

Example:
  $0 --host 192.168.0.254 --debug
TXT
}

sub normalize_udn {
  my ($udn) = @_;
  $udn //= '';
  $udn =~ s/^\s+|\s+$//g;
  $udn =~ s/^uuid://i;   # deine Bose/FHEM Beispiele sind ohne "uuid:"
  return $udn || 'UNKNOWN';
}

sub get_contentdirectory_control_and_udn {
  my ($ua, $desc_url) = @_;

  my $res = $ua->request(GET($desc_url));
  die "GET $desc_url failed: " . $res->status_line . "\n" if !$res->is_success;

  my $xml  = XML::LibXML->load_xml(string => $res->decoded_content);
  my $root = $xml->documentElement();

  my $u = URI->new($desc_url);
  my $base_url = $u->scheme . "://" . $u->host;
  $base_url .= ":" . $u->port if defined $u->port;

  my $udn = '';
  my ($udn_node) = $root->findnodes('//*[local-name()="UDN"]/text()');
  if ($udn_node) {
    $udn = $udn_node->data;
    $udn =~ s/^\s+|\s+$//g;
  }
  $udn ||= 'uuid:UNKNOWN';

  my ($service_type, $control_url);
  for my $svc ($root->findnodes('//*[local-name()="serviceList"]/*[local-name()="service"]')) {
    my ($st) = $svc->findnodes('./*[local-name()="serviceType"]/text()');
    next if !$st;
    my $stval = $st->data;
    next if $stval !~ /ContentDirectory/i;

    my ($cu) = $svc->findnodes('./*[local-name()="controlURL"]/text()');
    $control_url  = $cu ? $cu->data : undef;
    $service_type = $stval;
    last;
  }

  die "No ContentDirectory service in description.\n" if !$control_url || !$service_type;

  my $control_abs = URI->new_abs($control_url, $base_url)->as_string;
  return ($base_url, $control_abs, $service_type, $udn);
}

sub interactive_crawl {
  my %a = @_;

  my $ua           = $a{ua};
  my $base_url     = $a{base_url};
  my $control_url  = $a{control_url};
  my $service_type = $a{service_type};
  my $container_id = $a{container_id};   # << current container (this is what we need for location)
  my $depth        = $a{depth};
  my $maxdepth     = $a{maxdepth};
  my $seen         = $a{seen};
  my $dbg          = $a{debug};
  my $quit_ref     = $a{quit_ref};

  return if $$quit_ref;
  return if $depth > $maxdepth;
  return if $seen->{$container_id}++;

  print "Browse container id=$container_id depth=$depth\n" if $dbg;

  my $start = 0;
  my $count = 200;

  while (1) {
    return if $$quit_ref;

    my $r = soap_browse(
      ua           => $ua,
      control_url  => $control_url,
      service_type => $service_type,
      object_id    => $container_id,
      starting     => $start,
      requested    => $count,
    );

    my $entries = parse_didl($r->{Result}, $base_url);

    my $returned = $r->{NumberReturned} // 0;
    my $total    = $r->{TotalMatches}   // 0;

    for my $e (@$entries) {
      return if $$quit_ref;

      if ($e->{type} eq 'container') {
        interactive_crawl(
          %a,
          container_id => $e->{id},
          depth        => $depth + 1,
        );
      }
      elsif ($e->{type} eq 'item') {
        # Show metadata and ask for channel number
        show_item_with_container($e, $container_id);

        my $ans = ask_channel();

        if ($ans eq 'n') {
          print "-> übersprungen.\n\n";
          next;
        }
        if ($ans eq 'q') {
          $$quit_ref = 1;
          print "-> beendet.\n\n";
          return;
        }

        # numeric channel
        my $x = int($ans);

        if ($x >= 1 && $x <= 6) {
          # Preset: MUST use container location starting with "4"
          if ($container_id !~ /^4:/) {
            print "!! Abgelehnt: Für Preset 1..6 muss location ein Container sein, der mit '4:' beginnt.\n";
            print "   Aktueller Container: $container_id\n\n";
            next;
          }
          if ($a{preset_used}{$x}) {
            print "!! Abgelehnt: Preset-ID $x ist schon vergeben. (Nur 1..6, keine Duplikate)\n\n";
            next;
          }

          my $epoch = time();
          my $title = $e->{title} // '';
          my $logo  = $e->{logo}  // '';

          print {$a{fh_preset}} build_preset_xml(
            preset_id      => $x,
            epoch          => $epoch,
            location       => $container_id,         # << EXACTLY container id
            source_account => $a{source_account},
            item_name      => $title,
            container_art  => $logo,
          );

          $a{preset_used}{$x} = 1;
          print "-> Preset $x geschrieben.\n\n";
          next;
        }

        if ($x >= 10) {
          # FHEM attr: channel_xx
          my $title = $e->{title} // '';
          $title =~ s/\r|\n/ /g;

          my $logo = $e->{logo} // ''; # can be empty
          my $val  = join('|',
            '',            # we will inject " Stream" after title, and keep "||" structure below
          );

          # EXACT requested format:
          # attr <device> channel_{x} 80er Hits Stream||4:cont2:...|STORED_MUSIC|<sourceAccount>
          my $line = "attr $a{fhem_dev} channel_$x $title Stream||$container_id|STORED_MUSIC|$a{source_account}\n";
          print {$a{fh_attr}} $line;

          print "-> channel_$x geschrieben.\n\n";
          next;
        }

        # any other numeric
        print "!! Hinweis: Erlaubt sind Preset 1..6 oder channel >= 10. Eingabe '$x' wird ignoriert.\n\n";
      }
    }

    last if $returned <= 0;
    $start += $returned;
    last if $start >= $total;
  }
}

sub ask_channel {
  while (1) {
    print "Kanalnr (1..6 Preset, >=10 channel_xx) oder n/q: ";
    chomp(my $ans = <STDIN>);
    $ans = lc($ans // '');
    $ans =~ s/^\s+|\s+$//g;

    return 'n' if $ans eq 'n';
    return 'q' if $ans eq 'q';

    if ($ans =~ /^\d+$/) {
      return $ans;
    }
    print "Ungültig. Bitte Zahl, n oder q.\n";
  }
}

sub show_item_with_container {
  my ($e, $container_id) = @_;
  my $title = $e->{title} // '';
  my $id    = $e->{id}    // '';
  my $class = $e->{class} // '';
  my $res   = $e->{res}   // '';
  my $logo  = $e->{logo}  // '';

  print "----------------------------------------\n";
  print "Title:     $title\n";
  print "Item ID:   $id\n" if $id ne '';
  print "Class:     $class\n" if $class ne '';
  print "res:       $res\n"   if $res ne '';
  print "Logo:      $logo\n"  if $logo ne '';
  print "Container: $container_id\n";
  print "----------------------------------------\n";
}

sub xml_escape {
  my ($s) = @_;
  $s //= '';
  $s =~ s/&/&amp;/g;
  $s =~ s/</&lt;/g;
  $s =~ s/>/&gt;/g;
  $s =~ s/"/&quot;/g;
  $s =~ s/'/&apos;/g;
  return $s;
}

sub build_preset_xml {
  my %p = @_;
  my $pid   = $p{preset_id};
  my $epoch = $p{epoch};
  my $loc   = $p{location};
  my $sa    = $p{source_account};
  my $name  = $p{item_name} // '';
  my $art   = $p{container_art} // '';

  $name = xml_escape($name);
  $loc  = xml_escape($loc);
  $sa   = xml_escape($sa);

  my $xml = qq{<preset id="$pid" createdOn="$epoch" updatedOn="$epoch">\n}
          . qq{    <ContentItem source="STORED_MUSIC" location="$loc"\n}
          . qq{        sourceAccount="$sa" isPresetable="true">\n}
          . qq{        <itemName>$name</itemName>\n};

  if ($art ne '') {
    $art = xml_escape($art);
    $xml .= qq{        <containerArt>\n$art\n        </containerArt>\n};
  }

  $xml .= qq{    </ContentItem>\n}
       .  qq{</preset>\n};

  return $xml;
}

sub soap_browse {
  my %a = @_;
  my $ua           = $a{ua};
  my $control_url  = $a{control_url};
  my $service_type = $a{service_type};
  my $object_id    = $a{object_id};
  my $starting     = $a{starting}  // 0;
  my $requested    = $a{requested} // 200;

  my $soap = qq{<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <s:Body>
    <u:Browse xmlns:u="$service_type">
      <ObjectID>$object_id</ObjectID>
      <BrowseFlag>BrowseDirectChildren</BrowseFlag>
      <Filter>*</Filter>
      <StartingIndex>$starting</StartingIndex>
      <RequestedCount>$requested</RequestedCount>
      <SortCriteria></SortCriteria>
    </u:Browse>
  </s:Body>
</s:Envelope>};

  my $req = POST($control_url,
    Content_Type => 'text/xml; charset="utf-8"',
    Content      => $soap,
  );
  $req->header('SOAPACTION' => qq{"$service_type#Browse"});

  my $res = $ua->request($req);
  die "SOAP Browse failed ($control_url): " . $res->status_line . "\n" if !$res->is_success;

  my $xml = XML::LibXML->load_xml(string => $res->decoded_content);

  my ($result) = $xml->findnodes('//*[local-name()="BrowseResponse"]/*[local-name()="Result"]/text()');
  my ($nr)     = $xml->findnodes('//*[local-name()="BrowseResponse"]/*[local-name()="NumberReturned"]/text()');
  my ($tm)     = $xml->findnodes('//*[local-name()="BrowseResponse"]/*[local-name()="TotalMatches"]/text()');

  return {
    Result         => $result ? $result->data : '',
    NumberReturned => $nr     ? int($nr->data) : 0,
    TotalMatches   => $tm     ? int($tm->data) : 0,
  };
}

sub parse_didl {
  my ($didl_escaped, $base_url) = @_;
  return [] if !$didl_escaped;

  my $didl = $didl_escaped;

  # Sometimes escaped inside SOAP Result
  if ($didl =~ /&lt;DIDL-Lite/i) {
    $didl =~ s/&lt;/</g;
    $didl =~ s/&gt;/>/g;
    $didl =~ s/&quot;/"/g;
    $didl =~ s/&amp;/&/g;
  }

  my $doc;
  eval { $doc = XML::LibXML->load_xml(string => $didl); 1 } or return [];

  my @out;

  for my $c ($doc->findnodes('//*[local-name()="container"]')) {
    my $id = $c->getAttribute('id') // '';
    push @out, { type => 'container', id => $id };
  }

  for my $i ($doc->findnodes('//*[local-name()="item"]')) {
    my $id = $i->getAttribute('id') // '';
    my ($title) = $i->findnodes('./*[local-name()="title"]/text()');
    my ($class) = $i->findnodes('./*[local-name()="class"]/text()');
    my ($res)   = $i->findnodes('./*[local-name()="res"]/text()');

    my $logo = '';
    my ($art) = $i->findnodes('.//*[local-name()="albumArtURI"]/text()');
    if ($art) {
      $logo = $art->data;
      $logo =~ s/^\s+|\s+$//g;
      if ($logo && $logo !~ m{^https?://}i) {
        $logo = URI->new_abs($logo, $base_url)->as_string;
      }
    }

    push @out, {
      type  => 'item',
      id    => $id,
      title => $title ? $title->data : '',
      class => $class ? $class->data : '',
      res   => $res   ? $res->data   : '',
      logo  => $logo,
    };
  }

  return \@out;
}
#92
Wallboxen und E-Fahrzeuge / Aw: Wie findet man die passend...
Letzter Beitrag von betateilchen - 23 Februar 2026, 15:11:23
Zitat von: Ralli am 23 Februar 2026, 14:47:57Oder/und tatsächlich über Nacht ohne Wallbox einfach an der Haushaltssteckdose laden. Damit hast du gar keinen Investitionsaufwand.

Aber ich brauche ein 50 Meter langes Verlängerungskabel von meiner Terrasse einmal rund ums Mehrfamilienhaus  ;D
Der Technikraum des Hauses mit dem Zähler-/Verteilerkasten befindet sich fünf Meter vor meinem Stellplatz...

Jetzt warte ich erstmal die Antwort des Vermieters ab, dann überlege ich weiter.
#93
Forum-Software / Aw: FTUI3 ftui-label multiply
Letzter Beitrag von betateilchen - 23 Februar 2026, 15:05:47
was hat das mit "Forum Software" zu tun?

ZitatForum-Software
Regeln, Diskussionen, Fragen zu diesem FHEM-Forum selbst. Hier keine Themen zur Hausautomation!

Den Button zum Verschieben des Themas in den richtigen Forumbereich findest Du unten links auf der Seite.
#94
Perl für FHEM-User / Aw: Umrechnung von Stunden in ...
Letzter Beitrag von betateilchen - 23 Februar 2026, 15:04:38
Das lässt sich nicht so einfach mit Deiner Funktion umrechnen.
Innerhalb eines Zeitraumes von 5 Jahren können beispielsweise ein oder zwei Schaltjahre liegen.

Aber es gibt ja für perl durchaus Module, die solche Kalenderbe- und -umrechnungen unterstützen.
#95
Forum-Software / FTUI3 ftui-label multiply
Letzter Beitrag von amusilek - 23 Februar 2026, 14:55:31
Grüß Euch!

Ist es irgendwie möglich, den multiply Teil von ftui-label als reading aus einem Dummy device auszulesen?

<ftui-label size="3" unit="€"
              [text]="MQTT_Testplug:aenergy_total | divide(1000) | multiply([value] = 'Allerlei:Stromkosten') | round(1)"
              [color]="MQTT_Testplug:apower | step('0.0: orange, 0.01: white')">
</ftui-label>

Vielen Dank!
#96
Wallboxen und E-Fahrzeuge / Aw: Wie findet man die passend...
Letzter Beitrag von Ralli - 23 Februar 2026, 14:47:57
Zitat von: betateilchen am 23 Februar 2026, 14:34:19Ok, das mit der "einseitigen" Belastung habe ich verstanden.
Das bedeutet aber im Umkehrschluß, dass selbst bei einer 22kW Wallbox und einphasigem Laden keine 7,4kW rauskommen, denn das wäre ja auch wieder "einseitig".

Das ist korrekt.

ZitatEine Herdanschlussdose mit mehr als 5x2,5qmm Zuleitung ist mir in Privatwohnungen noch nicht untergekommen. Und die sind dann meist auch "nur" mit je 16A abgesichert. Der größere Querschnitt dürfte vermutlich nur der Dauerbelastung geschuldet sein.

2,5qmm passt genau für 32A - für 16A nimmt man standardmäßig 1,5qmm. Für Herdanschluss aber tatsächlich den größeren Querschnitt.

ZitatDa bleibt mir wohl als Option nur, auch weiterhin die Schnellladesäule von EnBW hier im Ort zu nutzen.

Oder/und tatsächlich über Nacht ohne Wallbox einfach an der Haushaltssteckdose laden. Damit hast du gar keinen Investitionsaufwand. Und was drin ist, ist drin.
#97
Anfängerfragen / Aw: Raspi4 startet nach Netzau...
Letzter Beitrag von Harald - 23 Februar 2026, 14:47:16
Hm, ich wußte bis gestern garnicht, daß es so ein Modul gibt. Das muß über autocreate ans Ende der fhem.cfg eingetragen worden sein, warum auch immer.
Wenn ich sudo systemctl start fhem eingebe, erhalte ich die Antwort Unit fhem.service not found
#98
Perl für FHEM-User / Umrechnung von Stunden in y d ...
Letzter Beitrag von Gisbert - 23 Februar 2026, 14:38:55
Hallo zusammen,

ich möchte eine Stundenzahl in ein Format y d h bringen, hab aber das Problem des Schaltjahres.
Mit folgfender Definition:
sub h2human($){
  my ($diff) = @_;
  my ($y,$d,$h,$ret);
  ($y,$diff) = _s2h_Div($diff,8766);
  ($d,$diff) = _s2h_Div($diff,24);
  ($h,$diff) = _s2h_Div($diff,1);
  $ret  = "$y"."y "."$d"."d "."$h"."h";
  return $ret;
}

sub _s2h_Div($$) {
  my ($p1,$p2) = @_;
  return (int($p1/$p2), $p1 % $p2);
}
gelingt es halb.
Für ein Jahr ohne Schalttag gibt es: 8760 Stunden.
Für ein Jahr mit Schalttag gibt es: 8784 Stunden, im Mittel jedes Jahr also 6 Stunden mehr.

Die obige Funktion macht die Berechnung so halb richtig, aber es fehlen ein paar Stunden. Konkret ergibt die Anzahl von 16290 Stunden 1y 313d 12h mit der obigen Funktion, aber es müssten 1y 313d 17h sein. Das ist jetzt nur ein kleiner Unterschied, aber ich hätte es gerne richtig, falls der Aufwand vertretbar ist.

Das genaue Datum, bei dem die Stundenzahl beginnt, ist prinzipiell bekannt - damit ist die Stundenzahl auch immer bekannt, aber die Angabe im Format y d h aber nicht.

Ich hoffe, das mein Anliegen mit dem konkreten Beispiel erkennbar ist.

Viele Grüße Gisbert
#99
Wallboxen und E-Fahrzeuge / Aw: Wie findet man die passend...
Letzter Beitrag von betateilchen - 23 Februar 2026, 14:34:19
Ok, das mit der "einseitigen" Belastung habe ich verstanden.
Das bedeutet aber im Umkehrschluß, dass selbst bei einer 22kW Wallbox und einphasigem Laden keine 7,4kW rauskommen, denn das wäre ja auch wieder "einseitig".

Zitat von: dieter114 am 23 Februar 2026, 12:05:50Also wenn Du also ein Fz einphasig mit mehr als 3,6kW laden willst
bist du eindeutig "außerhalb" der Legalität.

Das ist ja blöd...

Zitat von: Ralli am 23 Februar 2026, 14:18:15Dein Herd (nicht dein Ofen) sollte dreiphasig angeschlossen sein, sodass er auch bis max. 22kW (32A) aufnehmen dürfte

Eine Herdanschlussdose mit mehr als 5x2,5qmm Zuleitung ist mir in Privatwohnungen noch nicht untergekommen. Und die sind dann meist auch "nur" mit je 16A abgesichert. Der größere Querschnitt dürfte vermutlich nur der Dauerbelastung geschuldet sein.



Da bleibt mir wohl als Option nur, auch weiterhin die Schnellladesäule von EnBW hier im Ort zu nutzen.
#100
Wallboxen und E-Fahrzeuge / Aw: Wie findet man die passend...
Letzter Beitrag von Ralli - 23 Februar 2026, 14:18:15
Nein, die sind nicht illegal. Aber die müssen so konfiguriert sein, dass sie nur bis max. 4,6kW auf der einen Phase aufnehmen, so lange nicht auf den anderen Phasen gleichzeitig (durch andere Verbraucher) auch mehr Strom fließt, sodass die einzuhaltende maximale Schieflast nicht überschritten wird. 7,4kW (entspricht 32A auf einer Phase) wären also einphasig möglich, wenn durch andere Verbraucher auf den anderen zwei Phasen auch viel verbrauchen - das kommt bei einem Standard-Haushalt aber eher nicht vor.

Dein Herd (nicht dein Ofen) sollte dreiphasig angeschlossen sein, sodass er auch bis max. 22kW (32A) aufnehmen dürfte - was aber auch eher nicht der Praxis entspricht.

Vielleicht noch zur Ergänzung: P = U * I (Leistung = Spannung mal Stromstärke).
-> bei einer entsprechenden Absicherung und passendem Leiterquerschnitt kommt man so bei einem dreiphasigen Anschluss auf 22kW = 3 * 230V * 32A
-> bei einer entsprechenden Absicherung und passendem Leiterquerschnitt kommt man so bei einem einphasigen Anschluss auf 7,4kW = 1 * 230V * 32A
-> bei einem dreiphasigen Anschluss sorgt der Verbraucher dafür, dass allen drei Phasen gleich viel Leistung entnommen wird
-> bei einem einphasigen Anschluss kann der Verbraucher das nicht und muss aber die maximale Schieflast einhalten, daher ist normalerweise bei 4,6kW Schluss.