Alexa Sprachsteuerung von Plex per Custom Skill, Musik Ausgabe über Echo

Begonnen von stefanru, 22 Mai 2018, 21:05:57

Vorheriges Thema - Nächstes Thema

stefanru

Hi,

ich habe mir die letzten 2 Tage eine Musiksuche für meine lokale Musik über Amzon Echo gebaut.
Das ganze habe ich aus all den tollen Modulen und Beispielen hier zusammengebaut.
Vielen Dank an die Modul Autoren und alle die hier fleißig mit beitragen.
Ich möchte mein Beispiel hier niederschreiben, da es vieleicht ein paar Anregungen für eine AudioPlayer implementierung in das Alexa Modul gibt und eventuell auch Berücksichtigung bei einer Erweiterten Suchfunktion im Plex Modul.

Was man braucht:
In FHEM: Alexa Modul, Plex Modul (andere Musik Datenbank mit DLNA wäre auch möglich, dann muss die Ansteuerung und Suche in dieser aber ganz neu geschrieben werden).
Allgemein: einen HTTP Server mit SSL (offizielles Zertifikat, ich benutze Let's Encrypt), darauf gehe ich auch ein, einen Plex Server

Ok, zur Umsetzung:
In der 99_myUtils.pm habe ich folgendes zur Suche und erstellen der Playlist implementiert:
Am Anfang:
use List::MoreUtils qw(uniq);
use List::Util qw/shuffle/;


Die Sub:
# Alexa Plex Musik
sub Musikcmd ($)
{
    my ($artist) = @_;
 
my @artists;
my @albums;
my @rest1;
my @rest2;
my $found = 0;

# Token holen, ACHTUNG "Plex" ist der name des Plex devices und eventuell anzupassen
my $token = InternalVal("Plex", "token", undef);

my $return = "<speak>Entschuldige, das hat nicht geklappt.</speak>";
Log 1,"Ich habe ".$artist." verstanden.\n";


if ($artist =~ m,Playlist,i)
{
  my $search = fhem "get Plex m3u /playlists/85639/items";
  $search =~ s/file.mp3/file.mp3?X-Plex-Token=$token/g;
  $search =~ s/192.168.69.111/stefanru.noip.me/g;
  $search =~ s/http/https/g;
 
  open(M3U, ">/var/www/html/test.m3u"); 
  syswrite(M3U, $search);
  close(m3u);
  $found = 1;
}
elsif ($artist =~ m,stop,i)
    {
  #$return = "<myaudiostop>Lösche ...</myaudiostop>";
  my $search = "#Leer";
  open(M3U, ">/var/www/html/test.m3u"); 
  syswrite(M3U, $search);
  close(m3u);
  $found = 2;
}
else
    {
#Alte Liste Leeren
open(M3U, ">/var/www/html/test.m3u"); 
syswrite(M3U, "");
close(m3u);

  # Suche nach Artist bzw. Album
  my $search = fhem "get Plex search ".$artist;
  my @search_lines = split ("\n", $search);
  #print Dumper \@search_lines;
 
  @search_lines = @search_lines[ 3 .. $#search_lines ];
 
  print "Search Lines: \n";
  print Dumper \@search_lines;
 
  # Nach Artist und Album teilen
  foreach my $search_line (@search_lines)
  {  
    my @search_words = split ( / /, $search_line);
    if ($search_words[4] =~ m,artist,i)
{
  push @artists, $search_words[0];
    }
    elsif ($search_words[4] =~ m,album,i)
    {
     push @albums, $search_words[0];
    }
    else
    {
  push @rest1, $search_words[0];
    }  
  }
 
  print "Artists: \n";
  print Dumper \@artists;
 
  print "Albums: \n";
  print Dumper \@albums;
 
  print "Rest1: \n";
  print Dumper \@rest1;
 
 
  #Für Artists nochmal die Albums holen
  foreach my $search_artist (@artists)
  {
   my $search = fhem "get Plex ls ".$search_artist;
   my @search_lines = split ("\n", $search);
   @search_lines = @search_lines[ 4 .. $#search_lines ];
 
   foreach my $search_line (@search_lines)
   { 
    my @search_words = split ( / /, $search_line);

# Zu Albums hinzufügen
    if ($search_words[4] =~ m,album,i)
{
push @albums, $search_words[0];  
    }
    else
    {
  push @rest2, $search_words[0];
    }  
  }

 
  # Doppelte entfernen
  my @unique_albums = uniq(@albums);
 
  print "Albums: \n";
  print Dumper \@albums;
 
  print "Unique_albums: \n";
  print Dumper \@unique_albums;
 
  print "Rest2: \n";
  print Dumper \@rest2;
  # Reihenfolge der Albums Randomizen
  @unique_albums = shuffle @unique_albums;
 
  print "Unique_albums shuffle: \n";
  print Dumper \@unique_albums;
 
  # Playlist für alle Albums in M3U schreiben
  foreach my $album (@unique_albums)
  { 
   $search = fhem "get Plex m3u ". $album;
   $search =~ s/file.mp3/file.mp3?X-Plex-Token=$token/g;
   $search =~ s/192.168.69.111/stefanru.noip.me/g;
   $search =~ s/http/https/g;

   open(M3U, ">>/var/www/html/test.m3u"); 
   syswrite(M3U, $search);
   close(m3u);
   
   $found = 3;
  }

    } # Else kein Playlist. kein Stop
 
# Rückgabe, ACHTUNG zu lange Rückgaben verhindern den start der M3U
if ($found == 1)
{
$return = " <myaudiourl>https://stefanru.noip.me:8099/test.m3u</myaudiourl> <myaudiotext>Spiele Playlist</myaudiotext> ";
}
elsif ($found == 2)
{
$return = " <myaudiourl>https://stefanru.noip.me:8099/test.m3u</myaudiourl> <myaudiotext>Ich habe die Musik gestoppt</myaudiotext> ";
}
elsif ($found == 3)
{
$return = " <myaudiourl>https://stefanru.noip.me:8099/test.m3u</myaudiourl> <myaudiotext>Spiele ".$artist."</myaudiotext> ";
}
else
{
$return = "<speak>Entschuldige, das hat nicht geklappt. Ich habe für ".$artist." nichts gefunden. Versuche Playlist, den Namen eines Künstlers oder des Albums zu sagen oder stoppe die wiedergabe mit den Wörtern: Musik stop</speak>";
}

return $return;
}


Anpassungen an server.js in /opt/fhem/alexa-fhem/lib:
Zeile ca 920:
Nach der Abfrage für <speak>:
if( match = result.match( /^<speak>(.*)<\/speak>$/ ) ) {
                delete response.response.outputSpeech.text;
                response.response.outputSpeech.type = "SSML";
                response.response.outputSpeech.ssml = result;
              }


Hiernach muss folgendes eingefügt werden, um die eigenen Tags <myaudiourl> und <myaudiotext> zu verarbeiten:
              // fixme AudioPlayer Test
              if( match = result.match( /<myaudiourl>(.*)<\/myaudiourl>/ ) ) {
                  log.info ( 'Audiourl Match: ' + match[1] );
                  var txt = result.match( /<myaudiotext>(.*)<\/myaudiotext>/ )
                  log.info ( 'Audiotext Match: ' + txt[1] );
                  response.response = {
                    shouldEndSession: true,
                    outputSpeech: {                     
                                     type: "PlainText",
                                     text: txt[1]                     
                                  },
                    directives: [
                      {
                        type: "AudioPlayer.Play",
                        playBehavior: "REPLACE_ALL", // Setting to REPLACE_ALL means that this track will start playing immediately
                        audioItem: {
                            stream: {                         
                              url: match[1],
                              token: "1", // Unique token for the track - needed when queueing multiple tracks                         
                              offsetInMilliseconds: 0
                                    }
                                  }
                      }
                               ]
                                   }}


fhemIntents im Alexa device:
{Musikcmd ($artist)}=spiele {artist:SEARCH}
{Musikcmd ($artist)}=musik {artist:SEARCH}
{Musikcmd ($artist)}={artist:SEARCH}


Nun noch der Custom Skill, ich habe einen eigenen Angelegt parallel zum Alex Custom Skill.
Dieser muss dann auch in der json.conf eingetragen werden.
Wie das geht mehrere custom skills einzutragen ist bereits bei alexa fhem hier erklärt.

Skill Jason:
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "plex",
            "intents": [
                {
                    "name": "FHEMperlCodeIntent",
                    "slots": [
                        {
                            "name": "artist",
                            "type": "SEARCH"
                        }
                    ],
                    "samples": [
                        "spiele {artist}",
                        "spiel {artist}",
                        "musik {artist}",
                        "{artist}"
                    ]
                },
                {
                    "name": "AMAZON.PauseIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ResumeIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                }
            ],
            "types": [
                {
                    "name": "SEARCH",
                    "values": [
                        {
                            "name": {
                                "value": "Playlist"
                            }
                        },
                        {
                            "name": {
                                "value": "Stop"
                            }
                        },
                        {
                            "name": {
                                "value": "Göthe"
                            }
                        },
                        {
                            "name": {
                                "value": "Hallo Du"
                            }
                        }
                    ]
                }
            ]
        }
    }
}


Klar endpoints usw müsst ihr alles angeben wie in der Anleitung zum Custom Skill.
Bei Interface ist AudioPlayer anzuschalten.

Nun zur restlichen Infrastruktur.
Ich habe lighthttpd installiert. Ich habe einen Raspberry.
Einfach mit
sudo apt-get install lighttpd
installieren.

Plex habe ich auf einer WD my Cloud. Gibts aber auch für den Raspberry.

Nun muss das ganze noch https mit gültigem Zertifikat sprechen.
Zertifikate bekommt ihr bei Lets Encrypt.
Dazu braucht ihr den certbot. Beschrieben ist das sehr gut.

Ich gehe nun nur noch auf die automatische aktualisierung sowie die Aufbereitung der Zertifikate für Plex und lighthttpd ein:
Hierfür habe ich cron jobs:
00 4 * * 0 root /opt/fhem/certs/certbot-auto renew --quiet
02 4 * * 0 root /etc/letsencrypt/live/EureDomain/generate_pfx.sh
04 4 * * 0 root /etc/letsencrypt/live/EureDomain/generate_web.sh


Das generate_pfx.sh erstellt ein PFX File für Plex:
cd /etc/letsencrypt/live/EureDomain
openssl pkcs12 -export -out /mnt/OrtVonDemPlexDieDateiLesenKann/EureDomain.pfx -inkey privkey.pem -in cert.pem -certfile chain.pem -password pass:EuerPassword

Das entstehende EureDomain.pfx mit Pfad wo Plex es erreichen kann bei Plex->Einstellungen->Server->Netzwerk unter "Custom certificate location" eintragen.
Unter "Custom certificate encryption key" EuerPassword eintragen das ihr beim erstellen mitgebt.
Dann noch unter "Custom certificate domain" die Domain unter der ihr erreichbar seid und für die das Lets Encrypt Zertifikat ausgestellt ist

Das generate_web.sh erstellt das Zertifikat für lighthttpd und zwar so:
cat /etc/letsencrypt/live/EureDomain/cert.pem /etc/letsencrypt/live/EureDomain/privkey.pem > /etc/letsencrypt/live/EureDomain/web.pem

Nun noch die Einstellung für lighthttpd.conf unter /etc/lighthttpd:
server.port                 = 8099
ssl.engine = "enable"
ssl.pemfile = "/etc/letsencrypt/live/EureDomain/web.pem" # Combined Certificate
ssl.ca-file = "/etc/letsencrypt/live/EureDomaine/chain.pem" # Root CA
server.name = "EureDomain" # Domain Name OR Virtual Host Name

Der Port von Plex (normal 32400) und der Port von lighthttpd (hier 8099) müssen auf der Fritzbox freigegeben und an den jeweiligen Server geroutet werden.

So damit sollte es gehen.
Ich weiß, ganz einfach ist es nicht, aber ich habe das auf 2 Tage verteilt schnell zusammengebaut. Sollte also machbar sein.
Zu allem findet man auch Hilfe bei Google.

Ideal ist das ganze noch nicht. Ich benutze nur Playlist files im m3u Format. Angeblich braucht man nicht mal die mp3's auf nem https Server wenn man m3u8 schreibt (Habe ich nicht probiert).
In m3u's kann man nicht vorspringen usw. Auch das Stoppen ist ein billiger Hack mit ner leeren Playlist, da der Audioplayer stop nicht funktioniert hat. Denke das liegt auch an der m3u.

Alles in allem funktioniert es aber super! Die Erkennung geht sehr gut und es ist super seine Musik auf Alexa abzufragen.

Wenn Fragen sind versuche ich gerne diese zu Beantworten.
Wie gesagt es ist alles zusammen geschustert und sicher keine richtige Lösung, aber war auf die schnelle das Einfachste.

Ich bin gerne bereit bei der richtigen implementierung in das Alexa Modul zu helfen. Ich denke ich weiß wie man auch nur mit mp3's den Ausioplayer befüttert.
Dann geht auch next, previous und stop. Das war mir fürs erste einfach zu viel.

Viel Spaß,
Stefan




inesa394

Hallo

Ich hänge bei der Einbindung bei Amazon Del. fest, ein get MyAlexa interactionModel erzeugt zwar ein json in fhem der wird aber in der Form nicht mehr akzeptiert wenn ich den in den json Editor eingebe auch hat sich die Seite wohl ziemlich geändert seit meinen letzten Besuch dort. In Wiki wird man da leider auch nicht fündig.
Vieleicht kannst du mir ja helfen oder weißt wo man was findet hier im Forum konnte leider nichts finden dazu.

stefanru

Hi Inesa,

ja das vom Alexa Plugin generierte JSON Model geht schon lang nicht mehr zu kopieren.
Hier muss man etwas spielen und ausprobieren.
Ich hänge dir gerne meins an damit du eine Vorstellung bekommst wie es aussehen muss.


{
    "interactionModel": {
        "languageModel": {
            "invocationName": "plex",
            "intents": [
                {
                    "name": "FHEMperlCodeIntent",
                    "slots": [
                        {
                            "name": "artist",
                            "type": "SEARCH"
                        }
                    ],
                    "samples": [
                        "spiele {artist}",
                        "spiel {artist}",
                        "musik {artist}",
                        "spiele musik von {artist}",
                        "stoppe {artist}"
                    ]
                },
                {
                    "name": "AMAZON.PauseIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.ResumeIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": []
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": []
                }
            ],
            "types": [
                {
                    "name": "SEARCH",
                    "values": [
                        {
                            "name": {
                                "value": "Stoppe die Musik"
                            }
                        },
                        {
                            "name": {
                                "value": "Playlist"
                            }
                        },
                        {
                            "name": {
                                "value": "Stop"
                            }
                        },
                        {
                            "name": {
                                "value": "Göthe"
                            }
                        },
                        {
                            "name": {
                                "value": "Hallo Du"
                            }
                        }
                    ]
                }
            ]
        }
    }
}


Gruß,
Stefan

inesa394

Hallo

Kann es sein das durch diese Änderung im json einige fhemintents nicht mehr gehen.
Jedenfalls bei mir funktioniert die hälfte nicht mehr Alexa sagt nur noch ok aber nichts passiert.
Wie sowas hier {Alexacmd ("tvlauter")}=Fernseher lauter
{Alexacmd ("tvleiser")}=Fernseher leiser
{Alexacmd ("tvprgup")}=Fernseher nächstes Programm
{Alexacmd ("tvprgdown")}=Fernseher vorheriges Programm
{Alexacmd ("tverstes")}=Fernseher das erste
{Alexacmd ("tvzweites")}=Fernseher das zweite
{Alexacmd ("tvsateins")}=fernseher sat eins
{Alexacmd ("skyerstes")}=Fernseher sky eins
{Alexacmd ("skyzweites")}=fernseher sky zwei
{Alexacmd ("skysport")}=Fernseher sky sport eins
{Alexacmd ("skysport2")}=fernseher sky sport zwei

Werde wohl wohl oder übel alles neu anlegen müssen.
Hat sich in der Deklaration der fhem.intents was geändert ?

oder das hier
bildterrasse= bild von terrasse
bildwohnzimmer = bild vom wohnzimmer
bilddachdoben = bild vom dachboden
bildkeller = bild vom keller



([rr_inesa:state] eq "home" and [alexa:fhemIntent] eq "feuchtebad") (msg audio Aktuell haben wir im Bad eine Feuchte von 30 Prozent)
DOELSEIF ([rr_inesa:state] eq "asleep" and [alexa:fhemIntent] eq "gutenMorgen") (set rr_inesa home) ((msg audio einen guten morgen hast du gut geschlafen))
DOELSEIF ([rr_inesa:state] eq "home" and ["alexa:^fhemIntent"] eq "bildterrasse")  ({fhem ("get foscam image")}) (set telegram_inesa sendImage  /opt/fhem/FHEM/foscam_snapshot.jpg)
DOELSEIF ([rr_inesa:state] eq "home" and [alexa:fhemIntent] eq "bildwohnzimmer")  ( {system (' sudo /opt/fhem/xiaomisnapshot.sh')}) (set telegram_inesa sendImage /opt/fhem/FHEM/snapshot.jpg)
DOELSEIF ([rr_inesa:state] eq "home" and [alexa:fhemIntent] eq "bilddachboden") ({system (' sudo /opt/fhem/xiaomidachsnapshot.sh')}) (set telegram_inesa sendImage /opt/fhem/FHEM/dach.jpg)
DOELSEIF ([rr_inesa:state] eq "home" and [alexa:fhemIntent] eq "bildkeller") ({fhem ("get grete image")})(set telegram_inesa sendImage /opt/fhem/FHEM/grete_snapshot.jpg)
DOELSEIF ( [alexa:fhemIntent] eq "deckenlichtdunkler") (set Milight_Deckenleuchte dimdown 30)
DOELSEIF ( [alexa:fhemIntent] eq "deckenlichtheller") (set Milight_Deckenleuchte dimup 30)
DOELSEIF ( [alexa:fhemIntent] eq "bogenlampedunkler") (set Milight_Bogenlampe dimdown 30)
DOELSEIF ( [alexa:fhemIntent] eq "bogenlampeheller") (set Milight_Bogenlampe dimup 30)
DOELSEIF ( [alexa:fhemIntent] eq "fernsehlichtdunkler") (set Milight_TV dimdown 30)
DOELSEIF ( [alexa:fhemIntent] eq "fernsehlichtheller") (set Milight_TV dimup 30)
DOELSEIF ( [alexa:fhemIntent] eq "woisttelefon") (set anrufe call xxxxxxxxxx 30)
DOELSEIF ( [alexa:fhemIntent] eq "woistmeinhandy") (set anrufe call xxxxxxxxxx 30)


Inesa


stefanru

Hi Inesa,

habe jetzt erst registriert dass die Frage unter dem Plex Custom Skill steht.
Hast du damit ein Problem?
Ich habe dafür einen eigenen Skill angelegt, also es nicht mir in den FHEM Skill gepackt.
Schon allein weil ich ein anderes Aktivierungswort verwenden wollte.

Gruß,
Stefan

inesa394

Ja habe ich werde wohl auch wie du einen eigenen custom skill anlegen.Zur Zeit habe ich aber noch Problem den Plex
in Fhem vernünftig zum laufen zu bekommen. Ich kann zwar in meine datenbank durchsuchen aber er legt mir keine clienten an.Vieleicht liegt es ja daran das der Plex Server und das fhem Modul auf dem selben Rechner liegen.Was für eine Version Plex Server läuft bei dir?

stefanru

Hi,

mein Plex Server läuft auf meiner WD Cloud. Version ist: 1.13.2.5154
Aber der Server sollte für den Skill ausreichen.

Gruß,
Stefan

inesa394

Hab das jetzt mit Plex zum laufen gebracht lag wohl daran das fhem und Plex auf den selben Server lagen.
In den nächsten Tagen versuch ich dann mal
deine tut umzusetzen.

stefanru