TV Programm

Aus FHEMWiki

Sich das aktuelle Fernsehprogramm in FHEM anzeigen zu lassen, ist leider gar nicht so einfach. Die einzelnen Anbieter stellen leider keine entsprechenden Schnittstellen zur Verfügung, um auf einfache Art und Weise das aktuelle Fernsehprogramm für beliebige Sender einlesen und darstellen zu können. Mit ein paar Tricks funktioniert es aber trotzdem.

Hierfür gibt es gleich mehrere Ansätze:

  • Im einfachsten Fall bindet man sich ein iframe in die FHEM Weboberfläche ein. Einige Anbieter bieten sogar einen personalisierten Zugriff, so das man sich eine Übersicht nur mit den gewünschten Sendern zusammen stellen kann.
  • Es besteht die Möglichkeit EPG Daten in einem speziellen xmltv Format in die FHEM Installation zu laden (enthält das Programm für 6-7 Tage), diese XML Datei zu parsen und dann in FHEM z.B. über eine readingsGroup darzustellen.
  • Für Linux und auch Windows sind Tools verfügbar, mit denen man die Seiten von verschiedenen TV Programmanbietern grabben und daraus eine XML Datei im xmltv Format erstellen kann (enthaält das Programm von 1-14 Tagen je nach Anbieter). Einer der bekanntesten Vertreter ist WebGrab++. Diese XML Datei kann dann wieder in FHEM eingelesen und angezeigt werden z.B. über eine readingsGroup. Das parsen der Daten ist mit einem sehr hohen Traffic verbunden, so das hier die vorher erwähnten Methoden vorzuziehen sind.
  • Über httpmod könnte man eine Webseite eines TV Programmanbieters zyklisch einlesen und die für FHEM notwendigen Daten extrahieren. Von dieser Methode ist dringend abzuraten, da diese einen enormen Traffic sowohl für einen selbst, als auch für den Anbieter bedeutet. Aus diesem Grund soll diese Methode hier auch nicht dargestellt werden.

Variante 1 (iframe):

define wl_TV weblink iframe <Webseite des TV Programmanbieters z.B. http://www.klack.de/fernsehprogramm/was-laeuft-gerade/0/0/all.html>
attr wl_TV htmlattr width="1024" height="768"

Das Attribut legt die Größe des iframes fest und kann beliebig angepasst werden.

Variante 2 (Download der EPG Daten):

Dieser Ansatz ist bereits etwas komplizierter, aber immer noch sehr einfach einzubinden.

Vorbereitungen:

Fehlende Perl Module installieren:

sudo apt-get install libxml-bare-perl libdatetime-perl wget xz-utils

Die ersten beiden Bibliotheken werden benötigt, um die XML Datei zu parsen. wget wird benötigt um die Datei zu downloaden und xz enthält den Unpacker für die runtergeladene Datei.

Pfad für den Download anlegen und mit den entsprechenden Rechten versehen:

sudo mkdir /opt/fhem/tv
sudo chown fhem:dialout /opt/fhem/tv

In diesem Verzeichnis soll später die mit wget runtergeladene XML Datei landen.

99_myUtils.pm erweitern:

Dieser Code kann einfach in die Zwischenablage kopiert und in die Datei 99_myUtils.pm eingefügt werden.

sub rgUnfold($$)
{
  my ($device, $reading) = @_;
  my $title = ReadingsVal($device, $reading.'title', 'na');
  my $desc = ReadingsVal($device, $reading.'stitle', 'na')."\n\n".
             ReadingsVal($device, $reading.'desc', 'na');

  $title =~ s/(.{1,45}|\S{46,})(?:\s[^\S\r\n]*|\Z)/$1<br>/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('".$desc."')\">".$title."</a>";
}

sub xmltv2epoch($)
{
  my $dt = shift;
  
  if ($dt =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\s+([+-]\d{4}))?$/)
  {
    if (defined($7))
    {
      return $1.'-'.$2.'-'.$3.' '.$4.':'.$5.':'.$6.' '.$7;
    }
    else
    {
      return $1.'-'.$2.'-'.$3.' '.$4.':'.$5.':'.$6;
    }
  }
  
  return '2000-01-01 00:00:00';
}

sub tvParse($)
{
  use utf8;
  use Date::Parse;
  use Encode qw(encode_utf8 decode_utf8);
  use XML::Bare qw(forcearray);

  my $device = shift;
  my $hash = $defs{$device};
  my $obj = XML::Bare->new(file => '/opt/fhem/tv/rytecDE_Basic');
  my $xml = $obj->simple();
  my $start;
  my $stop;
  my $i = 0;
  my $fi = '000';
  my $lastChannel = '';
  my $reading = '';

  if (!$@)
  {
    # clear all old internals
    delete($hash->{helper});   
    
    readingsBeginUpdate($hash);

    foreach (@{forcearray($xml->{tv}{programme})})
    {
      # channel filter
      if ($_->{'channel'} =~ /^(?:ARD\.|ZDF\.|Sat1\.|RTL\.|RTL2\.|Pro7\.|DMax\.|Vox\.|Kabel\.)/)
      {
        $stop = str2time(xmltv2epoch($_->{'stop'}));
        
        # filter old stuff
        if ($stop >= time())
        {
          if ($lastChannel ne $_->{'channel'})
          {
            $lastChannel = $_->{'channel'};
            $reading = $_->{'channel'};
            $reading =~ s/\..*$//;
            $i = 0;
 
            $hash->{helper}{$reading.'_lastIndex'} = 0;
          }

          # limit number of readings
          next if ($i > 75);

          $fi = sprintf("%03d", $i);
          $start = str2time(xmltv2epoch($_->{'start'}));

          $hash->{helper}{$reading.'_'.$fi.'_bdate'} = substr(FmtDateTime($start), 0, 10);
          $hash->{helper}{$reading.'_'.$fi.'_btime'} = substr(FmtDateTime($start), 11, 8);
          $hash->{helper}{$reading.'_'.$fi.'_edate'} = substr(FmtDateTime($stop), 0, 10);
          $hash->{helper}{$reading.'_'.$fi.'_etime'} = substr(FmtDateTime($stop), 11, 8);
          $hash->{helper}{$reading.'_'.$fi.'_title'} = encode_utf8($_->{'title'}{'content'});
                    
          if (exists($_->{'sub-title'}{'content'}))
          {
            $hash->{helper}{$reading.'_'.$fi.'_stitle'} = encode_utf8($_->{'sub-title'}{'content'});
          }
          else
          {
            $hash->{helper}{$reading.'_'.$fi.'_stitle'} = 'na';
          }          
          
          if (exists($_->{'desc'}{'content'}))
          {
            $hash->{helper}{$reading.'_'.$fi.'_desc'} = encode_utf8($_->{'desc'}{'content'});
          }
          else
          {
            $hash->{helper}{$reading.'_'.$fi.'_desc'} = 'na';
          }

          if ($i < 3)
          {
            readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_bdate', substr(FmtDateTime($start), 0, 10)); 
            readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_btime', substr(FmtDateTime($start), 11, 8)); 
            readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_edate', substr(FmtDateTime($stop), 0, 10)); 
            readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_etime', substr(FmtDateTime($stop), 11, 8)); 
            readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_title', encode_utf8($_->{'title'}{'content'})); 
                    
            if (exists($_->{'sub-title'}{'content'}))
            {
              readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_stitle', encode_utf8($_->{'sub-title'}{'content'})); 
            }
            else
            {
              readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_stitle', 'na');
            }          
          
            if (exists($_->{'desc'}{'content'}))
            {
              readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_desc', encode_utf8($_->{'desc'}{'content'}));
            }
            else
            {
              readingsBulkUpdate($hash, 'next_'.$reading.'_'.$fi.'_desc', 'na');
            }
          }
          
          $i++;
        }
      }
    }

    readingsBulkUpdate($hash, 'state', 'parsed'); 
    readingsEndUpdate($hash, 0);
  }

  return undef;
}

sub tvDownload()
{
  my $output = qx(wget http://rytecepg.ipservers.eu/epg_data/rytecDE_Basic.xz -O /opt/fhem/tv/rytecDE_Basic.xz 2>&1);
  #print $output;
  $output = qx(xz -df /opt/fhem/tv/rytecDE_Basic.xz 2>&1);
  #print $output;
}

sub tvUpdate($)
{
  my $device = shift;
  my $hash = $defs{$device};
  my @channels = ( 'ARD', 'ZDF', 'Sat1', 'RTL', 'RTL2', 'Pro7', 'DMax', 'Vox', 'Kabel' );
  
  if (exists($hash->{helper}))
  {  
    readingsBeginUpdate($hash);

    foreach my $channel (@channels)
    {
      my $lastIndex = (exists($hash->{helper}{$channel.'_lastIndex'}) ? $hash->{helper}{$channel.'_lastIndex'} : undef);
      my $newLastIndex = $lastIndex;
      my $isNew = 1;
    
      if (defined($lastIndex))
      {
        my $i = 0;
        my $edate = (exists($hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_edate'}) ? $hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_edate'} : undef);

        while (($i < 3) && (defined($edate)))
        {
          my $index = sprintf("%03d", $lastIndex);
          my $etime = (exists($hash->{helper}{$channel.'_'.$index.'_etime'}) ? $hash->{helper}{$channel.'_'.$index.'_etime'} : undef);

          if ($edate.' '.$etime gt TimeNow())
          {
            my $nindex = sprintf("%03d", $i);

            if ($lastIndex == $newLastIndex)
            {
              $edate = undef;
              last;
            }

            if (1 == $isNew)
            {
              $hash->{helper}{$channel.'_lastIndex'} = $lastIndex;
              $isNew = 0;
            }

            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_bdate', $hash->{helper}{$channel.'_'.$index.'_bdate'});
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_btime', $hash->{helper}{$channel.'_'.$index.'_btime'}); 
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_edate', $edate); 
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_etime', $etime); 
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_title', $hash->{helper}{$channel.'_'.$index.'_title'}); 
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_stitle', $hash->{helper}{$channel.'_'.$index.'_stitle'}); 
            readingsBulkUpdate($hash, 'next_'.$channel.'_'.$nindex.'_desc', $hash->{helper}{$channel.'_'.$index.'_desc'});

            $i++;
          }

          $lastIndex++;
          $edate = (exists($hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_edate'}) ? $hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_edate'} : undef);
        }
      }
    }

    readingsBulkUpdate($hash, 'state', 'updated'); 
    readingsEndUpdate($hash, 1);
  }
  else
  {
    tvParse($device);
  }
}

sub tvUpdatePrimetime($)
{
  my $device = shift;
  my $hash = $defs{$device};
  my @channels = ( 'ARD', 'ZDF', 'Sat1', 'RTL', 'RTL2', 'Pro7', 'DMax', 'Vox', 'Kabel' );
  
  if (exists($hash->{helper}))
  {
    readingsBeginUpdate($hash);

    foreach my $channel (@channels)
    {
      my $lastIndex = (exists($hash->{helper}{$channel.'_lastIndex'}) ? $hash->{helper}{$channel.'_lastIndex'} : undef);
      my $newLastIndex = $lastIndex;
    
      if (defined($lastIndex))
      {
        my $i = 0;
        my $bdate = (exists($hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_bdate'}) ? $hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_bdate'} : undef);

        while (($i < 3) && (defined($bdate)))
        {
          my $index = sprintf("%03d", $lastIndex);
          my $btime = (exists($hash->{helper}{$channel.'_'.$index.'_btime'}) ? $hash->{helper}{$channel.'_'.$index.'_btime'} : undef);
          my $timeNow = substr(TimeNow(), 0, 11).'20:14:00';

          if ($bdate.' '.$btime gt $timeNow)
          {
            my $nindex = sprintf("%03d", $i);

            if ($lastIndex == $newLastIndex)
            {
              $bdate = undef;
              last;
            }

            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_bdate', $bdate);
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_btime', $btime); 
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_edate', $hash->{helper}{$channel.'_'.$index.'_edate'}); 
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_etime', $hash->{helper}{$channel.'_'.$index.'_etime'}); 
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_title', $hash->{helper}{$channel.'_'.$index.'_title'}); 
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_stitle', $hash->{helper}{$channel.'_'.$index.'_stitle'}); 
            readingsBulkUpdate($hash, 'prime_'.$channel.'_'.$nindex.'_desc', $hash->{helper}{$channel.'_'.$index.'_desc'}); 

            $i++;
          }

          $lastIndex++;
          $bdate = (exists($hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_bdate'}) ? $hash->{helper}{$channel.'_'.sprintf("%03d", $lastIndex).'_bdate'} : undef);
        }
      }
    }

    readingsBulkUpdate($hash, 'state', 'updated'); 
    readingsEndUpdate($hash, 1);
  }
}

Dummy Device anlegen:

Dieses Device dient zur Datenhaltung. Hier werden immer die nächsten 3 Sendungen und die 3 Primetime Sendungen des aktuellen Tages als Readings abgelegt. Zusätzlich dazu wird intern eine kleine Datenbank aufgebaut und aktualisiert, damit die große XML Datei nicht ständig neu eingelsen und geparsed werden muss.

define dmy_TV dummy

at Devices anlegen:

4 "at" Devices müssen angelegt werden. Eins für den Download (alle 3 Tage 1x), eins für das Parsen der Daten ins Dummy Device (jeden Tag 1x), eins um die nächsten Sendungen zu filtern (alle 15min) und noch eins um die Primetime Sendungen zu filtern (jeden Tag 1x). Die 4 "at" sind als raw Definitionen kopiert und können auch als solche wieder angelegt werden. Dazu irgend ein Device öffnen, ganz unten auf "Raw definition" klicken und alles entfernen. Den Code von hier einfügen und ausführen und die Devices sind angelegt. Jedes at Device muss separat angelegt werden!

defmod at_TV_DOWNLOAD at *00:15:00 {if ((1 == $wday) || (4 == $wday)) {tvDownload()}}
defmod at_TV_PARSE at *00:30:00 {tvParse('dmy_TV')}
defmod at_TV_UPDATE_PRIME at *00:45:00 {tvUpdatePrimetime('dmy_TV')}
defmod at_TV_UPDATE at +*00:15:00 {tvUpdate('dmy_TV')}