Fernsehprogramm DOIF (aktuell und primetime | non blocking | minimaler traffic)

Begonnen von mumpitzstuff, 12 Juni 2020, 21:47:48

Vorheriges Thema - Nächstes Thema

mumpitzstuff

Ich habe mich mal hingesetzt und ein DOIF generiert, das die aktuellen Fernsehdaten einer Woche als xml von einem Server lädt (das DOIF macht das 1x alle 3-4 Tage) und daraus eine Programmzeitschrift generiert. Sowohl der Download als auch die Generierung geschehen non blocking. Es werden 2 Dinge generiert. Zum Einen die nächsten 3 Sendungen ab jetzt und zum Anderen die 3 Sendungen ab 20:15 Uhr. Die einzelnen Sendungen sind anklickbar und zeigen dann eine detaillierte Beschreibung zu der Sendung.

Nachdem das DOIF erstellt wurde, dauert es 5min bis die aktuellen Sendungen sichtbar werden. Die Prime Sendungen sind nach weiteren 5 Minuten sichtbar. Wenn die Prime Sendungen zwischen 20:15 Uhr und 0:00 aktualisiert werden, dann laufen sie quasi mit, da keine veralteten Sendungen angezeigt werden. Erst nach 0:30 werden dann die wirklichen Prime Sendungen des nächsten Tages (20:15 Uhr Sendungen) angezeigt.

Es gibt genau eine Vorbedingung, damit das DOIF funktioniert (alle Dinge ab 2. sind optional).

1.) XML::Bare muss mind. in der Version 0.53 installiert sein. Das kann man z.B. hiermit erledigen:

sudo apt-get install libxml-bare-perl
2.) Will man sich die Senderlogos anzeigen lassen, dann muss das Verzeichnis "/opt/fhem/www/images/default/tv" erstellt und die Rechte so gesetzt sein, das FHEM darauf zugreifen kann. Danach muss man die Senderlogos als PNGs in dieses Verzeichnis kopieren und ebenfalls die entsprechenden Rechte setzen. Die Dateien müssen die richtigen Namen besitzen! Diese kann man in der ersten Spalte der Darstellung sehen. Steht hier zum Beispiel "tv/ard" dann muss die Datei "/opt/fhem/www/images/default/tv/ard.png" heissen. Eine Quelle dafür ist z.B. (meine Logos kann ich leider aus rechtlichen Gründen hier nicht zur Verfügung stellen, aber höflich per PM fragen funktioniert vielleicht...):

https://github.com/3PO/Senderlogos/archive/master.zip

https://github.com/picons/picons/archive/master.zip

https://de.wikipedia.org/wiki/Liste_deutschsprachiger_Fernsehsender

https://iptv-org.github.io

SVG Logos gibt es z.B. hier (habe ich noch nie verwendet, sollte aber auch gehen):

https://commons.wikimedia.org/wiki/Category:SVG_logos_of_television_channels_and_networks?uselang=de

Ansonsten gibt es diverse Programmezeitschriften online, bei denen man sich die Bilder der Sender runterladen kann.

3.) Wenn man Sender hinzufügen möchte, dann kann man sich die original xml Dateien runterladen und die entsprechenden Sender raus suchen.

http://rytecepg.dyndns.tv/epg_data/

Danach muss man folgende Dinge anpassen (wenn man einen Sender entfernen möchte, muss man die entsprechenden Dinge ebenfalls anpassen):
a) Die Variable $_channelFilter.
b) Im Attribut uiTable muss man 2 Zeilen hinzufügen: "TPL_TV($SELF,next,<sender name>,<logo name>)" und weiter unten "TPL_TV($SELF,prime,<sender name>,<logo name>)"
c) Danach muss man noch die Formatierung ebenfalls im Attribut uiTable anpassen. Diese beginnen mit $TR und $TD.

Die xml Daten stellen jede Menge weiterer Sender zur Verfügung.

Unten habe ich auch mal ein Testscript dl.pl angehangen, womit man seinen channelfilter erst einmal überprüfen kann. Einfach den channelfilter im Script anpassen und ausführen und man sieht das Ergebnis erst einmal in einer Testdatei und kann, wenn alles funktioniert, das Regex ins DOIF übertragen. Es ist wichtig, das man immer den Text aus der id nimmt!

<channel id="ARD.de">
4.) Es stehen 2 Templates zur Verfügung. TPL_TV wird normalerweise verwendet und stellt das Programmlogo und 3 Sendezeiten dar. Darüber hinaus kann auch TPL_TVSET verwendet werden. Hier kann durch ein Klick auf das Programmlogo ein set Befehl an ein beliebiges FHEM Device geschickt werden, um z.B. den Fernsehsender aus FHEM heraus zu wechseln.

Beispiel:
TPL_TV($SELF,next,ARD,ard)kann ersetzt werden durch:
TPL_TVSET($SELF,next,ARD,ard,irblasterdevice,send IR_SAT_DasErste)
5.) Wenn man andere Sender oder mehr als die vorhandenen haben möchte, kann man z.B. hier sehen was es sonst noch alles gibt:

http://rytecepg.dyndns.tv/epg_data/

Im DOIF werden nur die beiden Dateien rytecDE_Basic.xz und rytecDE_Common.xz geladen und gemerged. Das bdedeutet, das man aktuell nur Sender aus diesen beiden Dateien anzeigen lassen kann. Es gibt aber die Möglichkeit weitere Dateien zu mergen und dann ebenfalls anzeigen zu lassen wie z.B. rytecDE_SportMovies.xz.

Dazu muss man einfach die Funktion "sub tvDownloadMerge()" suchen und diese ersetzen:

  sub tvDownloadMerge()
  {
    my $output = '';

    ## other server
    ## http://www.xmltvepg.nl
    ## http://rytecepg.epgspot.com/epg_data
    ## http://epg.vuplus-community.net
    ## http://rytecepg.wanwizard.eu
    ## http://rytecepg.dyndns.tv/epg_data
    ## datafiles: rytecDE_Basic.xz, rytecDE_Common.xz, rytecDE_SportMovies.xz
    ## alternative: https://iptv-org.github.io/epg/guides/de.xml (must be downloaded every day!)
    $output .= qx(wget $_server/rytecDE_Basic.xz -O $_path/rytecDE_Basic.xz 2>&1 || rm -f $_path/rytecDE_Basic.xz 2>&1);
    $output .= qx(xz -df $_path/rytecDE_Basic.xz 2>&1);
    $output .= qx(wget $_server/rytecDE_Common.xz -O $_path/rytecDE_Common.xz 2>&1 || rm -f $_path/rytecDE_Common.xz 2>&1);
    $output .= qx(xz -df $_path/rytecDE_Common.xz 2>&1);
    ##$output .= qx(wget $_server/rytecDE_SportMovies.xz -O $_path/rytecDE_SportMovies.xz 2>&1 || rm -f $_path/rytecDE_SportMovies.xz 2>&1);
    ##$output .= qx(xz -df $_path/rytecDE_SportMovies.xz 2>&1);
    ## dont forget to change the following line from "if ([00:00:30|Mo Do])" to "if ([00:00:30])"
    ##$output .= qx(wget https://iptv-org.github.io/epg/guides/de.xml.gz -O $_path/rytecDE_Custom.gz 2>&1 || rm -f $_path/rytecDE_Custom.gz 2>&1);
    ##$output .= qx(gzip -d $_path/rytecDE_Custom.gz 2>&1);
   
    ## download and merge other files here if needed

    tvMerge($_dataFile, $_path.'/rytecDE_Common');
    ##tvMerge($_dataFile, $_path.'/rytecDE_Common', $_path.'/rytecDE_SportMovies');
    ##tvMerge($_path.'/rytecDE_Custom');

    return $output;
  }

durch

  sub tvDownloadMerge()
  {
    my $output = '';

    ## other server
    ## http://www.xmltvepg.nl
    ## http://rytecepg.epgspot.com/epg_data
    ## http://epg.vuplus-community.net
    ## http://rytecepg.wanwizard.eu
    ## http://rytecepg.dyndns.tv/epg_data
    ## datafiles: rytecDE_Basic.xz, rytecDE_Common.xz, rytecDE_SportMovies.xz
    ## alternative: https://iptv-org.github.io/epg/guides/de.xml (must be downloaded every day!)
    $output .= qx(wget $_server/rytecDE_Basic.xz -O $_path/rytecDE_Basic.xz 2>&1 || rm -f $_path/rytecDE_Basic.xz 2>&1);
    $output .= qx(xz -df $_path/rytecDE_Basic.xz 2>&1);
    $output .= qx(wget $_server/rytecDE_Common.xz -O $_path/rytecDE_Common.xz 2>&1 || rm -f $_path/rytecDE_Common.xz 2>&1);
    $output .= qx(xz -df $_path/rytecDE_Common.xz 2>&1);
    $output .= qx(wget $_server/rytecDE_SportMovies.xz -O $_path/rytecDE_SportMovies.xz 2>&1 || rm -f $_path/rytecDE_SportMovies.xz 2>&1);
    $output .= qx(xz -df $_path/rytecDE_SportMovies.xz 2>&1);
    ## dont forget to change the following line from "if ([00:00:30|Mo Do])" to "if ([00:00:30])"
    ##$output .= qx(wget https://iptv-org.github.io/epg/guides/de.xml.gz -O $_path/rytecDE_Custom.gz 2>&1 || rm -f $_path/rytecDE_Custom.gz 2>&1);
    ##$output .= qx(gzip -d $_path/rytecDE_Custom.gz 2>&1);
   
    ## download and merge other files here if needed

    ##tvMerge($_dataFile, $_path.'/rytecDE_Common');
    tvMerge($_dataFile, $_path.'/rytecDE_Common', $_path.'/rytecDE_SportMovies');
    ##tvMerge($_path.'/rytecDE_Custom');

    return $output;
  }

ersetzen. Das Spiel lässt sich beliebig fortsetzen.

Im Folgenden ist meine Version zu sehen, allerdings hat ein anderer User ebenfalls eine modifizierte Version zur Verfügung gestellt. Diese könnt ihr hier finden: https://forum.fhem.de/index.php/topic,112081.msg1255378.html#msg1255378

defmod doif_TEST DOIF subs\
{\
  use utf8;;\
  use Date::Parse;;\
  ## sudo apt-get install libxml-bare-perl\
  use XML::Bare 0.53 qw(forcearray);;\
  use Blocking;;\
  ##use Encode qw(encode_utf8 decode_utf8);;\
\
  ### CONFIG AREA ###\
  $_channelFilter = qr/^(?:DasErste\.|ZDF\.|ZDFinfo\.|Sat1\.|RTL2?\.|Pro(Sieben|SiebenMaxx)\.|DMax\.|Vox\.|KabelEins(?:Classic|Doku)?\.|ntv\.|Sixx\.|TLC\.|N24Doku\.|SonyEntertainmentTV\.|AnimalPlanet\.|History\.|Kinowelt\.|NatGeo(?:HD|Wild)\.|GeoTV\.|CuriosityChannel\.|Sky1\.|WELT\.|phoenix\.|ServusHD\.|BILD\.|Silverline\.|13thStreet\.|AXN\.|SciFi\.|CrimeInvestigation\.|ComedyCentralVIVA\.|Universal\.|DiscoveryHD\.|eSports1\.)/;;\
  ## telnet port must not be password protected (open)\
  ## this is used as fallback if telnet port cannot created automatically\
  $_telnetPort = 7072;;\
  ## can be used to adjust the program times (mostly not needed!)\
  $_timeAdjust = 0;;\
  ## path to fhem installation\
  $_path = '/opt/fhem';;\
  ## path to datafile to be parsed to get the tv data\
  $_dataFile = $_path.'/rytecDE_Basic';;\
  ## download server to be used\
  $_server = AttrVal("$SELF", "server", "http://www.xmltvepg.nl");;\
  ## enable/disable unused channel filtering on filemerge (enabled = small file and less readings = faster)\
  $_filterChannels = 1;;\
  ## enable/disable updates based on starttimes (enabled = update channels only if needed = faster)\
  $_updateBasedOnStarttimes = 1;;\
  ## keep tv data in RAM (faster but needs more RAM and the initial load data call is BLOCKING / disable it if you are low on RAM)\
  $_keepDataInRam = 0;;\
  ## enable/disable use of Time::Piece (timepiece is faster but not installed on some systems (sudo cpanm Time::Piece))\
  $_timepiece = 0;;\
\
  ## internal variables\
  $_startTimes = ();;\
  $_tvData = ();;\
  $_isDataLoaded = 0;;\
\
\
  sub filterText($)\
  {\
    $_[0] =~ s/[\x{0022}\x{0060}\x{003b}\x{0027}\"\`;;\'\r]//g;;\
    $_[0] =~ s/[\n]/<br>/g;;\
\
    return $_[0];;\
  }\
\
  sub xmltv2epoch($)\
  {\
    my $t = shift;;\
\
    if ($_timepiece)\
    {\
      use Time::Piece;;\
\
      ## fast version\
      return Time::Piece->strptime($t, '%Y%m%d%H%M%S %z')->epoch;;\
    }\
    else\
    {\
      ## slow but compatible version\
      substr($t, 8, 0) = 'T';;\
\
      return str2time($t);;\
    }\
  }\
\
  sub FmtDateTime($)\
  {\
    my @t = localtime(shift);;\
    return sprintf("%04d-%02d-%02d %02d:%02d:%02d", $t[5]+1900, $t[4]+1, $t[3], $t[2], $t[1], $t[0]);;\
  }\
\
  sub createTelnet($)\
  {\
    my $device = shift;;\
    my $telnet = undef;;\
\
    foreach my $d (sort keys %::defs) \
    {\
      next if ($d !~ /telnetForTvUpdateFn_\d+/);;\
      my $h = $::defs{$d};;\
      next if (!$h->{TYPE} || $h->{TYPE} ne 'telnet' || $h->{SNAME});;\
      next if (::AttrVal($d, 'allowfrom', '127.0.0.1') ne '127.0.0.1');;\
      next if ($h->{DEF} !~ /^\d+( global)?$/);;\
      next if ($h->{DEF} =~ /IPV6/);;\
\
      $telnet = $d;;\
      last;;\
    }\
\
    if (!defined($telnet))\
    {\
      $telnet = 'telnetForTvUpdateFn_'.time();;\
      my $ret = ::CommandDefine(undef, "-temporary $telnet telnet 0");;\
\
      if (defined($ret))\
      {\
        ::Log3 $device, 1, $device.': Cannot create telnet port ('.$ret.')';;\
        return undef;;\
      }\
\
      $::attr{$telnet}{room} = 'hidden';;\
      $::attr{$telnet}{allowfrom} = '127.0.0.1';;\
    }\
\
    return $::defs{$telnet}{PORT};;\
  }\
\
  sub tvParse($$$)\
  {\
    my ($device, $mode, $port) = @_;;\
    my $obj;;\
    my $xml;;\
    my $tvDataRef;;\
    my $lastChannel = '';;\
    my $reading = '';;\
    my $i = 999;;\
    my $n = 999;;\
    my $k = 0;;\
    my $primeTime = substr(FmtDateTime(time() + $_timeAdjust), 0, 11).'20:14:00';;\
    my $sendTelnet = '';;\
    my $old = time() + $_timeAdjust;;\
\
    ## check if any update is needed\
    if ((0 != $_updateBasedOnStarttimes) && ('prime' ne $mode) && keys(%{$_startTimes}))\
    {\
      my $nothingTodo = 1;;\
      foreach (keys(%{$_startTimes}))\
      {\
        if ($_startTimes{$_} <= $old)\
        {\
          $nothingTodo = 0;;\
          ::Log3 $device, 4, $device.': Update is not blocked because at least one actual program is finished (reading: '.$_.', start: '.$_startTimes{$_}.', old: '.$old.').';;\
          last;;\
        }\
      }\
      \
      if (0 != $nothingTodo)\
      {\
        ::Log3 $device, 4, $device.': Update is blocked because no actual program was finished.';;\
        return %{$_startTimes};;\
      }\
    }\
    \
    if ((0 == $_keepDataInRam) || (0 == $_isDataLoaded))\
    {\
        $obj = XML::Bare->new(file => $_dataFile);;\
        $xml = $obj->parse() if (!$@);;\
        $tvDataRef = \@{forcearray($xml->{'tv'}{'programme'})} if (!$@);;\
    }\
    else\
    {\
        $tvDataRef = \@{$_tvData};;\
    }\
\
    if (!$@)\
    {\
      ## preallocate memory\
      vec($sendTelnet, 10000, 8) = 0;;\
      $sendTelnet = '';;\
      \
      foreach (@{$tvDataRef})\
      {\
        my $stop = xmltv2epoch($_->{'stop'}{'value'});;\
        \
        ## filter old stuff\
        if ($stop > $old)\
        {\
          if ($lastChannel ne $_->{'channel'}{'value'})\
          {\
            $lastChannel = $_->{'channel'}{'value'};;\
            $reading = $_->{'channel'}{'value'};;\
            $reading =~ s/[\.\s]//g;;\
            $reading =~ s/de$//;;\
            $n = 0;;\
\
            if ((0 == $_updateBasedOnStarttimes) || !exists($_startTimes{$reading}) || ($_startTimes{$reading} <= $old))\
            {\
              $i = 0;;\
\
              if (0 != $_updateBasedOnStarttimes)\
              {\
                $_startTimes{$reading} = $stop;;\
              }\
            }\
            else\
            {\
              ::Log3 $device, 4, $device.': '.$reading.' is blocked because actual program is not finished (reading: '.$reading.', start: '.$_startTimes{$reading}.', old: '.$old.').';;\
            }\
          }\
\
          if (($i < 3) && ('next' eq $mode))\
          {\
            my $fi = sprintf("%03d", $i);;\
            my $start = xmltv2epoch($_->{'start'}{'value'});;\
            \
            my $starttime = substr(FmtDateTime($start), 0, 19);;\
            my $oldstarttime = get_Reading('next_'.$reading.'_'.$fi.'_btime');;\
            \
            ## prevent any update which is not required\
            if ($starttime ne $oldstarttime)\
            {\
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' next_'.$reading.'_'.$fi.'_btime ';;\
              $sendTelnet .= $starttime;;\
\
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' next_'.$reading.'_'.$fi.'_title ';;\
              $sendTelnet .= filterText(@{forcearray($_->{'title'})}[0]->{'value'});;\
\
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' next_'.$reading.'_'.$fi.'_desc ';;\
              if (exists($_->{'sub-title'}{'value'}))\
              {\
                $sendTelnet .= filterText($_->{'sub-title'}{'value'}).'\n\n';;\
              }\
              else\
              {\
                $sendTelnet .= 'na\n\n';;\
              }\
              if (exists($_->{'desc'}{'value'}))\
              {\
                $sendTelnet .= filterText($_->{'desc'}{'value'});;\
              }\
              else\
              {\
                $sendTelnet .= 'na';;\
              }\
\
              $k++;;\
            }\
            \
            $i++;;\
          }\
\
          if (($n < 3) && ('prime' eq $mode))\
          {\
            my $start = xmltv2epoch($_->{'start'}{'value'});;\
            my $fmtStart = FmtDateTime($start);;\
            my $bdate = substr($fmtStart, 0, 10);;\
            my $btime = substr($fmtStart, 11, 8);;\
\
            if ($bdate.' '.$btime gt $primeTime)\
            {\
              my $fn = sprintf("%03d", $n);;\
\
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' prime_'.$reading.'_'.$fn.'_btime ';;\
              $sendTelnet .= substr(FmtDateTime($start), 0, 19);;\
                            \
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' prime_'.$reading.'_'.$fn.'_title ';;\
              $sendTelnet .= filterText(@{forcearray($_->{'title'})}[0]->{'value'});;\
              \
              $sendTelnet .= ";;setreading $device";;\
              $sendTelnet .= ' prime_'.$reading.'_'.$fn.'_desc ';;\
              if (exists($_->{'sub-title'}{'value'}))\
              {\
                $sendTelnet .= filterText($_->{'sub-title'}{'value'}).'\n\n';;\
              }\
              else\
              {\
                $sendTelnet .= 'na\n\n';;\
              }\
              if (exists($_->{'desc'}{'value'}))\
              {\
                $sendTelnet .= filterText($_->{'desc'}{'value'});;\
              }\
              else\
              {\
                $sendTelnet .= 'na';;\
              }\
                            \
              $k++;;\
              $n++;;\
            }\
          }\
\
          if ($k >= 10)\
          {\
            ##::Log3 $device, 5, $device.': '.encode_utf8($sendTelnet);;\
            \
            `perl /opt/fhem/fhem.pl $port "$sendTelnet"`;;\
            \
            $k = 0;;\
            $sendTelnet = '';;\
          }\
        }\
      }\
\
      if ('' ne $sendTelnet)\
      {\
        ##::Log3 $device, 5, $device.': '.encode_utf8($sendTelnet);;\
\
        `perl /opt/fhem/fhem.pl $port "$sendTelnet"`;;\
      }\
    }\
\
    return %{$_startTimes};;\
  }\
\
  sub tvMerge\
  {\
    my ($dstName, @srcNames) = @_;;\
    my $fh;;\
    my $data;;\
    my $start = '';;\
    my $channels = '';;\
    my $channels_flt = '';;\
    my $programms = '';;\
    my $programms_flt = '';;\
    my $end = '';;\
    my $pos;;\
\
    if (-e $dstName)\
    {\
      open($fh, '<', $dstName) or die "Can't open file $!";;\
      read($fh, $data, -s $fh);;\
      close($fh);;\
\
      if (-1 != ($pos = index($data, '<channel ')))\
      {\
        $start = substr($data, 0, $pos);;\
      }\
\
      if (-1 != ($pos = rindex($data, '</programme>')))\
      {\
        $end = substr($data, $pos + 12);;\
      }\
\
      for (my $i = 0;; $i < (scalar(@srcNames) + 1);; $i++)\
      {\
        if (0 != $i)\
        {\
          my $file = $srcNames[$i - 1];;\
\
          if (-e $file)\
          {\
            open($fh, '<', $file) or die "Can't open file $!";;\
            read($fh, $data, -s $fh);;\
            close($fh);;\
          }\
          else\
          {\
            last;;\
          }\
        }\
\
        while ($data =~ /(\s*<channel\s.*?id="(.*?)".*?<\/channel>)/sg)\
        {\
          if (0 != $_filterChannels)\
          {\
            $_ = $1;;\
\
            if ($2 =~ $_channelFilter)\
            {\
              $channels_flt .= $_;;\
            }\
          }\
          else\
          {\
            $channels .= $1;;\
          }\
        }\
\
        while ($data =~ /(\s*<programme\s.*?channel="(.*?)".*?<\/programme>)/sg)\
        {\
          if (0 != $_filterChannels)\
          {\
            $_ = $1;;\
\
            if ($2 =~ $_channelFilter)\
            {\
              $programms_flt .= $_;;\
            }\
          }\
          else\
          {\
            $programms .= $1;;\
          }\
        }\
      }\
\
      open($fh, '>', $dstName) or die "Can't open file $!";;\
\
      if (0 != $_filterChannels)\
      {\
        print $fh $start.$channels_flt.$programms_flt.$end;;\
      }\
      else\
      {\
        print $fh $start.$channels.$programms.$end;;\
      }\
\
      close($fh);;\
    }\
  }\
\
  sub tvDownload()\
  {\
    my $output = '';;\
\
    ## other server see below\
    $output .= qx(wget $_server/rytecDE_Basic.xz -O $_path/rytecDE_Basic.xz 2>&1 || rm -f $_path/rytecDE_Basic.xz 2>&1);;\
    $output .= qx(xz -df $_path/rytecDE_Basic.xz 2>&1);;\
\
    if (0 != $_filterChannels)\
    {\
      tvMerge($_dataFile);;\
    }\
\
    return $output;;\
  }\
\
  sub tvDownloadMerge()\
  {\
    my $output = '';;\
\
    ## other server\
    ## http://www.xmltvepg.nl\
    ## http://rytecepg.epgspot.com/epg_data\
    ## http://epg.vuplus-community.net\
    ## http://rytecepg.wanwizard.eu\
    ## http://rytecepg.dyndns.tv/epg_data\
    ## datafiles: rytecDE_Basic.xz, rytecDE_Common.xz, rytecDE_SportMovies.xz\
    ## alternative: https://iptv-org.github.io/epg/guides/de.xml (must be downloaded every day!)\
    $output .= qx(wget $_server/rytecDE_Basic.xz -O $_path/rytecDE_Basic.xz 2>&1 || rm -f $_path/rytecDE_Basic.xz 2>&1);;\
    $output .= qx(xz -df $_path/rytecDE_Basic.xz 2>&1);;\
    $output .= qx(wget $_server/rytecDE_Common.xz -O $_path/rytecDE_Common.xz 2>&1 || rm -f $_path/rytecDE_Common.xz 2>&1);;\
    $output .= qx(xz -df $_path/rytecDE_Common.xz 2>&1);;\
    ##$output .= qx(wget $_server/rytecDE_SportMovies.xz -O $_path/rytecDE_SportMovies.xz 2>&1 || rm -f $_path/rytecDE_SportMovies.xz 2>&1);;\
    ##$output .= qx(xz -df $_path/rytecDE_SportMovies.xz 2>&1);;\
    ## dont forget to change the following line from "if ([00:00:30|Mo Do])" to "if ([00:00:30])"\
    ##$output .= qx(wget https://iptv-org.github.io/epg/guides/de.xml.gz -O $_path/rytecDE_Custom.gz 2>&1 || rm -f $_path/rytecDE_Custom.gz 2>&1);;\
    ##$output .= qx(gzip -d $_path/rytecDE_Custom.gz 2>&1);;\
    \
    ## download and merge other files here if needed\
\
    tvMerge($_dataFile, $_path.'/rytecDE_Common');;\
    ##tvMerge($_dataFile, $_path.'/rytecDE_Common', $_path.'/rytecDE_SportMovies');;\
    ##tvMerge($_path.'/rytecDE_Custom');;\
\
    return $output;;\
  }\
\
  sub startDownload($)\
  {\
    my $name = shift;;\
\
    ## prevent download spamming\
    if (-e $_dataFile)\
    {\
      my $ftime = ((time() - (stat($_dataFile))[9]) / 60.0 / 60.0 / 24.0);;\
\
      if ($ftime < 1.0)\
      {\
        ::Log3 $name, 1, $name.': Download of TV data skipped because file is not older than 1 day ('.($ftime).').';;\
        return;;\
      }\
    }\
\
    if (defined($_blockingcalls{PID_DOWNLOAD}))\
    {\
      ::Log3 $name, 3, $name.': Blocking call already running (download).';;\
\
      ::BlockingKill($_blockingcalls{PID_DOWNLOAD});;\
    }\
\
    $_blockingcalls{PID_DOWNLOAD} = ::BlockingCall('DOIF::doDownload', $name, 'DOIF::endDownload', 300, 'DOIF::abortDownload', $name);;\
  }\
\
  sub DOIF::doDownload($)\
  {\
    my $name = shift;;\
    my $output = '';;\
\
    $output = tvDownloadMerge();;\
\
    return $name.'|'.$output;;\
  }\
  \
  sub DOIF::endDownload($)\
  {\
    my ($name, $output) = split("\\|", shift);;\
\
    ::Log3 $name, 5, $name.': '.$output;;\
    ::Log3 $name, 4, $name.': Blocking call finished to download tv data.';;\
    \
    ## be aware that this call is BLOCKING!\
    if (0 != $_keepDataInRam)\
    {\
        my $obj = XML::Bare->new(file => $_dataFile);;\
        my $xml = $obj->parse() if (!$@);;\
        $_tvData = @{forcearray($xml->{'tv'}{'programme'})} if (!$@);;\
        if (!$@)\
        {\
          $_isDataLoaded = 1;;\
        }\
        else\
        {\
          $_isDataLoaded = 0;;\
        }\
    }\
\
    delete($_blockingcalls{PID_DOWNLOAD});;\
  }\
\
  sub DOIF::abortDownload($)\
  {\
    my $name = shift;;\
\
    delete($_blockingcalls{PID_DOWNLOAD});;\
\
    ::Log3 $name, 1, $name.': Blocking call aborted (download).';;\
  }\
\
  sub startParse($$)\
  {\
    my ($name, $mode) = @_;;\
    my $port;;\
\
    if (defined($_blockingcalls{PID_PARSE}))\
    {\
      ::Log3 $name, 3, $name.': Blocking call already running (parse).';;\
\
      ::BlockingKill($_blockingcalls{PID_PARSE});;\
    }\
\
    $port = createTelnet($name);;\
    $port = $_telnetPort if (!defined($port));;\
\
    $_blockingcalls{PID_PARSE} = ::BlockingCall('DOIF::doParse', $name.'|'.$mode.'|'.$port, 'DOIF::endParse', 300, 'DOIF::abortParse', $name);;\
  }\
\
  sub DOIF::doParse($)\
  {\
    my ($name, $mode, $port) = split("\\|", shift);;\
    my $ret = $name;;\
    my %startTimes = tvParse($name, $mode, $port);;\
\
    foreach (keys(%startTimes))\
    {\
      $ret .= '|'.$_.'|'.$startTimes{$_};;\
    }\
    \
    return $ret;;\
  }\
\
  sub DOIF::endParse($)\
  {\
    my ($name, @newStartTimes) = split("\\|", shift);;\
\
    for (my $i = 0;; $i < scalar(@newStartTimes);; $i += 2)\
    {\
      $_startTimes{$newStartTimes[$i]} = $newStartTimes[$i + 1];;\
    }\
    \
    ::Log3 $name, 4, $name.': Blocking call finished to parse tv data.';;\
\
    delete($_blockingcalls{PID_PARSE});;\
  }\
\
  sub DOIF::abortParse($)\
  {\
    my $name = shift;;\
\
    delete($_blockingcalls{PID_PARSE});;\
\
    ::Log3 $name, 1, $name.': Blocking call aborted (parse).';;\
  }\
}\
init\
{\
  startDownload("$SELF");;\
  set_Exec('init_next', 180, 'startParse("$SELF", "next")');;\
  set_Exec('init_prime', 420, 'startParse("$SELF", "prime")');;\
}\
{[00:00:30|Mo Do];;startDownload("$SELF")}\
## start in a raster of 5min\
{[+:05];;startParse("$SELF", 'next')}\
## start between next updates\
{[00:32:30];;startParse("$SELF", 'prime')}\

attr doif_TEST userattr server:http://www.xmltvepg.nl,http://epg.vuplus-community.net,http://rytecepg.epgspot.com/epg_data,http://rytecepg.wanwizard.eu,http://rytecepg.dyndns.tv/epg_data
attr doif_TEST alias Aktuelles TV-Programm
attr doif_TEST event-on-change-reading .*
attr doif_TEST room TV
attr doif_TEST server http://www.xmltvepg.nl
attr doif_TEST uiTable {\
  package ui_Table;;\
\
  $SHOWNOSTATE = 1;;\
  $ATTRIBUTESFIRST = 1;;\
    \
  ## 39 normal channels, 2 heading lines and 38 prime channels\
  ## 41 = 39 (normal channels) + 2 (headings)\
  ## 79 = 39 (normal channels) + 2 (headings) + 38 (prime channels)\
  ## 40 = 39 (normal channels) + 1 (first heading)\
  $TR{0,41} = "style='color:yellow;;;;text-align:center;;;;font-weight:bold;;;;font-size:18px'";;\
  $TD{0..39,41..79}{2,4} = "style='font-size:16px;;;;border-right-style:solid;;;;border-color:#CCCCCC;;;;border-right-width:1px;;;;'";;\
  $TD{0..39,41..79}{0} = "align='center' style='border-right-style:solid;;;;border-color:#CCCCCC;;;;border-right-width:1px;;;;'";;\
  $TD{0..79}{1,3,5,6} = "style='font-size:16px;;;;'";;\
  $TD{40}{0..6} = "style='border-top-style:solid;;;;border-bottom-style:solid;;;;border-color:#CCCCCC;;;;border-top-width:1px;;;;border-bottom-width:1px;;;;'";;\
\
  sub showIcon\
  {\
    my ($icon, $device, $state) = @_;;\
    \
    if (defined($device) && defined($state))\
    {\
      return "<a href=\"$::FW_ME?cmd=set $device $state$::FW_CSRF\">".ICON("tv/$icon")."</a>";;\
    }\
    else\
    {\
      return ICON("tv/$icon");;\
    }\
  }\
  \
  sub showIconIP\
  {\
    my ($icon, $device, $state) = @_;;\
\
    if (defined($device) && defined($state))\
    {\
      return "<a href=\"". ::ReadingsVal("$SELF", "$device", "") =~ s/.state/$state/r ."\" target=\"IPTV\">".ICON("tv/$icon")."</a>";;\
    }\
    else\
    {\
      return ICON("tv/$icon");;\
    }\
  }\
  \
  sub unfold\
  {\
    ##my ($title, $desc) = @_;;\
\
    ##$title = 'na' if (!defined($title));;\
    ##$desc = 'na'."\n\n".'na' if (!defined($desc));;\
    \
    ##$title =~ s/(.{1,45}|\S{46,})(?:\s[^\S\r\n]*|\Z)/$1<br>/g;;\
    ##$desc =~ s/<br>/\n/g;;\
    ##$desc =~ s/(.{1,65}|\S{66,})(?:\s[^\S\r\n]*|\Z)/$1<br>/g;; \
    ##$desc =~ s/[\r\'\"]/ /g;;\
    ##$desc =~ s/[\n]|\\n/<br>/g;;\
\
    ##return "<a href=\"#!\" onclick=\"FW_okDialog(&#39;;".$desc."&#39;;)\">".$title."</a>";;\
    \
    $_[0] = 'na' if (!defined($_[0]));;\
    $_[1] = 'na'."\n\n".'na' if (!defined($_[1]));;\
    \
    $_[0] =~ s/(.{1,45}|\S{46,})(?:\s[^\S\r\n]*|\Z)/$1<br>/g;;\
    $_[1] =~ s/<br>/\n/g;;\
    $_[1] =~ s/(.{1,65}|\S{66,})(?:\s[^\S\r\n]*|\Z)/$1<br>/g;; \
    $_[1] =~ s/[\r\'\"]/ /g;;\
    $_[1] =~ s/[\n]|\\n/<br>/g;;\
\
    return "<a href=\"#!\" onclick=\"FW_okDialog(&#39;;".$_[1]."&#39;;)\">".$_[0]."</a>";;\
  }\
}\
\
## parameter: device name, mode (next or prime), channel name (see xml data file), icon name (filename of channel logo)\
DEF TPL_TV(showIcon("$4",undef,undef)|substr([$1:$2_$3_000_btime],11,5)|unfold([$1:$2_$3_000_title],[$1:$2_$3_000_desc])|substr([$1:$2_$3_001_btime],11,5)|unfold([$1:$2_$3_001_title],[$1:$2_$3_001_desc])|substr([$1:$2_$3_002_btime],11,5)|unfold([$1:$2_$3_002_title],[$1:$2_$3_002_desc]))\
\
## parameter: device name, mode (next or prime), channel name (see xml data file), icon name (filename of channel logo), device name for set command, command\
## example: TPL_TVSET($SELF,next,DasErste,ard,<ir blaster device>,<ir blaster command>)\
DEF TPL_TVSET(showIcon("$4","$5","$6")|substr([$1:$2_$3_000_btime],11,5)|unfold([$1:$2_$3_000_title],[$1:$2_$3_000_desc])|substr([$1:$2_$3_001_btime],11,5)|unfold([$1:$2_$3_001_title],[$1:$2_$3_001_desc])|substr([$1:$2_$3_002_btime],11,5)|unfold([$1:$2_$3_002_title],[$1:$2_$3_002_desc]))\
\
## parameter: device name, mode (next or prime), channel name (see xml data file), icon name (filename of channel logo), device name for set command, command\
## example: TPL_TVIP($SELF,next,DasErste,ard,VIEW1,29438503040)\
DEF TPL_TVIP(showIconIP("$4","$5","$6")|substr([$1:$2_$3_000_btime],11,5)|unfold([$1:$2_$3_000_title],[$1:$2_$3_000_desc])|substr([$1:$2_$3_001_btime],11,5)|unfold([$1:$2_$3_001_title],[$1:$2_$3_001_desc])|substr([$1:$2_$3_002_btime],11,5)|unfold([$1:$2_$3_002_title],[$1:$2_$3_002_desc]))\
\
"Sender"|"ab"|"Aktuelle Sendung"|"ab"|"Nächste Sendung"|"ab"|"Sendung"\
TPL_TV($SELF,next,DasErste,ard)\
TPL_TV($SELF,next,ZDF,zdf)\
TPL_TV($SELF,next,Sat1,sat1)\
TPL_TV($SELF,next,RTL,rtl)\
TPL_TV($SELF,next,RTL2,rtl2)\
TPL_TV($SELF,next,ProSieben,pro7)\
TPL_TV($SELF,next,DMax,dmax)\
TPL_TV($SELF,next,Vox,vox)\
TPL_TV($SELF,next,KabelEins,kabel1)\
TPL_TV($SELF,next,KabelEinsClassic,kabel1classic)\
TPL_TV($SELF,next,13thStreet,13thstreet)\
TPL_TV($SELF,next,Silverline,silverline)\
TPL_TV($SELF,next,AXN,axn)\
TPL_TV($SELF,next,SonyEntertainmentTV,sonytv)\
TPL_TV($SELF,next,Kinowelt,kinowelt)\
TPL_TV($SELF,next,ProSiebenMaxx,pro7maxx)\
TPL_TV($SELF,next,Sixx,sixx)\
TPL_TV($SELF,next,Universal,universal)\
TPL_TV($SELF,next,SciFi,syfy)\
TPL_TV($SELF,next,ComedyCentralVIVA,comedycentral)\
TPL_TV($SELF,next,CrimeInvestigation,crimeinvest)\
TPL_TV($SELF,next,ntv,ntv)\
TPL_TV($SELF,next,N24Doku,n24)\
TPL_TV($SELF,next,phoenix,phoenix)\
TPL_TV($SELF,next,ZDFinfo,zdfinfo)\
TPL_TV($SELF,next,History,history)\
TPL_TV($SELF,next,KabelEinsDoku,kabel1doku)\
TPL_TV($SELF,next,WELT,welt)\
TPL_TV($SELF,next,ServusHD,servus)\
TPL_TV($SELF,next,BILD,bild)\
TPL_TV($SELF,next,AnimalPlanet,animalplanet)\
TPL_TV($SELF,next,GeoTV,geotv)\
TPL_TV($SELF,next,NatGeoHD,natgeo)\
TPL_TV($SELF,next,NatGeoWild,natgeowild)\
TPL_TV($SELF,next,DiscoveryHD,discovery)\
TPL_TV($SELF,next,Sky1,skyone)\
TPL_TV($SELF,next,CuriosityChannel,curiosity)\
TPL_TV($SELF,next,TLC,tlc)\
TPL_TV($SELF,next,eSports1,esports1)\
"&nbsp;;"|"&nbsp;;"|"&nbsp;;"|"&nbsp;;"|"&nbsp;;"|"&nbsp;;"|"&nbsp;;"\
"Sender"|"ab"|"Sendung"|"ab"|"Sendung"|"ab"|"Sendung"\
TPL_TV($SELF,prime,DasErste,ard)\
TPL_TV($SELF,prime,ZDF,zdf)\
TPL_TV($SELF,prime,Sat1,sat1)\
TPL_TV($SELF,prime,RTL,rtl)\
TPL_TV($SELF,prime,RTL2,rtl2)\
TPL_TV($SELF,prime,ProSieben,pro7)\
TPL_TV($SELF,prime,DMax,dmax)\
TPL_TV($SELF,prime,Vox,vox)\
TPL_TV($SELF,prime,KabelEins,kabel1)\
TPL_TV($SELF,prime,KabelEinsClassic,kabel1classic)\
TPL_TV($SELF,prime,13thStreet,13thstreet)\
TPL_TV($SELF,prime,Silverline,silverline)\
TPL_TV($SELF,prime,AXN,axn)\
TPL_TV($SELF,prime,SonyEntertainmentTV,sonytv)\
TPL_TV($SELF,prime,Kinowelt,kinowelt)\
TPL_TV($SELF,prime,ProSiebenMaxx,pro7maxx)\
TPL_TV($SELF,prime,Sixx,sixx)\
TPL_TV($SELF,prime,Universal,universal)\
TPL_TV($SELF,prime,SciFi,syfy)\
TPL_TV($SELF,prime,ComedyCentralVIVA,comedycentral)\
TPL_TV($SELF,prime,CrimeInvestigation,crimeinvest)\
TPL_TV($SELF,prime,ntv,ntv)\
TPL_TV($SELF,prime,N24Doku,n24)\
TPL_TV($SELF,prime,phoenix,phoenix)\
TPL_TV($SELF,prime,ZDFinfo,zdfinfo)\
TPL_TV($SELF,prime,History,history)\
TPL_TV($SELF,prime,KabelEinsDoku,kabel1doku)\
TPL_TV($SELF,prime,WELT,welt)\
TPL_TV($SELF,prime,ServusHD,servus)\
TPL_TV($SELF,prime,BILD,bild)\
TPL_TV($SELF,prime,AnimalPlanet,animalplanet)\
TPL_TV($SELF,prime,GeoTV,geotv)\
TPL_TV($SELF,prime,NatGeoHD,natgeo)\
TPL_TV($SELF,prime,NatGeoWild,natgeowild)\
TPL_TV($SELF,prime,DiscoveryHD,discovery)\
TPL_TV($SELF,prime,Sky1,skyone)\
TPL_TV($SELF,prime,CuriosityChannel,curiosity)\
TPL_TV($SELF,prime,TLC,tlc)

Damian

Ich setze den Post für ne Weile hoch. Es könnte für den einen oder anderen interessant sein. Es ist offenbar etwas mehr als nur DOIF-DOELSEIF-DOELSE, das, was die meisten unter DOIF verstehen ;)
Programmierte FHEM-Module: DOIF-FHEM, DOIF-Perl, DOIF-uiTable, THRESHOLD, FHEM-Befehl: IF

cwagner

Zitat von: Damian am 12 Juni 2020, 22:02:49
Ich setze den Post für ne Weile hoch. Es könnte für den einen oder anderen interessant sein. Es ist offenbar etwas mehr als nur DOIF-DOELSEIF-DOELSE, das, was die meisten unter DOIF verstehen ;)
Diese "Adelung" ist für mich gerechtfertigt - ein toller Showcase für die Mächtigkeit von DOIF.

Christian
PI 2B+/3B+ Raspbian 12, Perl 5.36.0, FHEM 6.3: 295 Module in ConfigDB: Steuerung Heizkessel, FBH, Solarthermie, kontr. Lüftung mit WRG. Smarthome u.a. HMCUL, 1-Wire (FT232RL ; DS2480B), EnOcean (TCM EPS3), MQTT2. DOIF, PID20, Threshold, OWX; Micropelt IRTV, Volkszähler, SolarForecast; MariaDB

amenomade

Warum der Umweg über Telnet ? Wenn ich nichts übersehen habe, machst Du nur "setreading" Kommandos?

EDIT: ah ok verstanden. Du machst verschiedene Sachen in externalisierten Prozesse.
Pi 3B, Alexa, CUL868+Selbstbau 1/2λ-Dipol-Antenne, USB Optolink / Vitotronic, Debmatic und HM / HmIP Komponenten, Rademacher Duofern Jalousien, Fritz!Dect Thermostaten, Proteus

mumpitzstuff

Ich habe zuerst versucht die Daten als return zurück zu geben, aber das hat mir das gesamte System abstürzen lassen. Der Weg über Telnet ging dagegen erst einmal problemlos. Ich bin aber noch dran und versuche genau diesen Punkt noch zu optimieren. Vielleicht finde ich noch einen eleganteren Weg.

mumpitzstuff

Es ist leider nicht möglich auf den separaten Telnet Port zu verzichten. Wenn das Blocking Modul alle Daten auf einmal per Telnet aus dem Fork in den Hauptprozess pushen will, dann geschehen sehr merkwürdige Dinge aufgrund der Menge an Daten. Genaueres konnte ich leider nicht rausbekommen, aber der Datentransfer im Blocking Modul stürzt entweder komplett ab oder überträgt nur einen Teil der Daten. Das endet irgndwann im Datenchaos. Ich muss daher erst einmal bei dieser Variante bleiben. Ich könnte lediglich versuchen, so wie das Blocking Modul auch, eine temporäre Telnet Verbindung zu erstellen und nach der Übertragung wieder zu löschen. Dann muss sich der Anwender nicht darum kümmern.

Invers

Hi mumpitzstuff. Super Sache. Habe ich so toll noch nicht gesehen. Danke.

Ich habe auch mal probiert.
Bei mir dauerte es lange, bis etwas passierte. Das stört mich aber nicht.

Die Senderlogos sehe ich nicht. Vermutlich, weil Pfad und Dateien nicht existieren.
Verrätst du mir, woher du die geladen hast?
Pi3B+ mit SSD/ Bullseye | FB7590 AX | 12 x Dect200 | CUL433+868 | SDuino | HM-LAN | 3 x Heizung FHT + FKontakte | KeyMatic + 4 FB | HM Wandtaster 2-fach m. LED | 6 x Türkont. TFK-TI | HM-Bew.-Melder innen | 3 x Smoked. HM-SEC-SD-2

carlos

Ich habe sie mir mal von
https://download.avm.de/tv/
geholt und nach /opt/fhem/www/images/tv kopiert.

Funktioniert aber nicht!

Gruß

Carlos
FHEM svn auf Intel NUC mit proxmox,1 UDOO, 3 Raspberry Pi, signalduino, nanoCUL, div. Homematic Komponenten, toom Baumarkt Funksteckdosen, einige sonoffs, hue, shelly

amenomade

Zitat von: carlos am 14 Juni 2020, 10:58:52
Ich habe sie mir mal von
https://download.avm.de/tv/
geholt und nach /opt/fhem/www/images/tv kopiert.

Funktioniert aber nicht!

Gruß

Carlos

Klar. Wenn Du eine andere Quelle benutzt, musst Du natürlich die Parse Funktion entspr. anpassen.
Pi 3B, Alexa, CUL868+Selbstbau 1/2λ-Dipol-Antenne, USB Optolink / Vitotronic, Debmatic und HM / HmIP Komponenten, Rademacher Duofern Jalousien, Fritz!Dect Thermostaten, Proteus

carlos

Die werden aber bei mir und bei invers überhaupt nicht angezeigt, da steht z.b. nur "tv/ard".

Gruß

Carlos
FHEM svn auf Intel NUC mit proxmox,1 UDOO, 3 Raspberry Pi, signalduino, nanoCUL, div. Homematic Komponenten, toom Baumarkt Funksteckdosen, einige sonoffs, hue, shelly

Invers

Pi3B+ mit SSD/ Bullseye | FB7590 AX | 12 x Dect200 | CUL433+868 | SDuino | HM-LAN | 3 x Heizung FHT + FKontakte | KeyMatic + 4 FB | HM Wandtaster 2-fach m. LED | 6 x Türkont. TFK-TI | HM-Bew.-Melder innen | 3 x Smoked. HM-SEC-SD-2

amenomade

Zitat von: Invers am 14 Juni 2020, 12:54:26
Genau so ist es leider. Keine Anzeige.

Gut... von mir falsch verstanden. Ihr meint die Icons?
Dann macht bitte ein ls -la /opt/fhem/www/images/tv

Und ein "list" vom DOIF kann sowieso nicht schaden.
Pi 3B, Alexa, CUL868+Selbstbau 1/2λ-Dipol-Antenne, USB Optolink / Vitotronic, Debmatic und HM / HmIP Komponenten, Rademacher Duofern Jalousien, Fritz!Dect Thermostaten, Proteus

Invers

ls: Zugriff auf '/opt/fhem/www/images/tv' nicht möglich: Datei oder Verzeichnis nicht gefunden
p

hätte ich dir aber auch so sagen können. Deshalb fragte ich ja, woher die Icons kommen.
Pi3B+ mit SSD/ Bullseye | FB7590 AX | 12 x Dect200 | CUL433+868 | SDuino | HM-LAN | 3 x Heizung FHT + FKontakte | KeyMatic + 4 FB | HM Wandtaster 2-fach m. LED | 6 x Türkont. TFK-TI | HM-Bew.-Melder innen | 3 x Smoked. HM-SEC-SD-2

carlos

Bei mir 1 zu 1 aus deinem ersten Beitrag kopiert.
Das verzeichnis /opt/fhem/www/images/tv  gab es bei mir nicht.
Da habe ich die logos von AVM hinkopiert.
Gruß

Carlos

FHEM svn auf Intel NUC mit proxmox,1 UDOO, 3 Raspberry Pi, signalduino, nanoCUL, div. Homematic Komponenten, toom Baumarkt Funksteckdosen, einige sonoffs, hue, shelly

Invers

Ich habe da noch eine Zusatzfrage: Wo finde ich denn die Schreibweise des Senders, wenn ich das DOIF um Tele5 erweitern möchte?
Pi3B+ mit SSD/ Bullseye | FB7590 AX | 12 x Dect200 | CUL433+868 | SDuino | HM-LAN | 3 x Heizung FHT + FKontakte | KeyMatic + 4 FB | HM Wandtaster 2-fach m. LED | 6 x Türkont. TFK-TI | HM-Bew.-Melder innen | 3 x Smoked. HM-SEC-SD-2