70_XBMC.pm:

-added commands: openmovieid, openepisodeid, addon, jsonraw (thanks to siggi85)
-fixed fork attribute to close file handles correctly
-added mechanism to detect disconnects (TCP)
-improved message parsing
-some code refactoring
-improved code formatting

git-svn-id: svn://svn.code.sf.net/p/fhem/code/trunk@7443 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
vbs2
2015-01-05 12:26:15 +00:00
parent df9bb36e7f
commit e577816be0

View File

@@ -29,7 +29,7 @@ sub XBMC_Initialize($$)
$hash->{ReadFn} = "XBMC_Read";
$hash->{ReadyFn} = "XBMC_Ready";
$hash->{UndefFn} = "XBMC_Undefine";
$hash->{AttrList} = "fork:enable,disable compatibilityMode:xbmc,plex offMode:quit,hibernate,shutdown,standby " . $readingFnAttributes;
$hash->{AttrList} = "fork:enable,disable compatibilityMode:xbmc,plex offMode:quit,hibernate,shutdown,standby pingInterval " . $readingFnAttributes;
$data{RC_makenotify}{XBMC} = "XBMC_RCmakenotify";
$data{RC_layout}{XBMC_RClayout} = "XBMC_RClayout";
@@ -69,6 +69,9 @@ sub XBMC_Define($$)
else {
return "Username and/or password missing.";
}
$attr{$hash->{NAME}}{"pingInterval"} = 60;
return undef;
}
@@ -84,6 +87,20 @@ sub XBMC_Ready($)
elsif(!$hash->{CHILDPID}) {
return if($hash->{CHILDPID} = fork);
my $ppid = getppid();
### Copied from Blocking.pm
foreach my $d (sort keys %defs) { # Close all kind of FD
my $h = $defs{$d};
#the following line was added by vbs to not close parent's DbLog DB handle
$h->{DBH}->{InactiveDestroy} = 1 if ($h->{TYPE} eq 'DbLog');
TcpServer_Close($h) if($h->{SERVERSOCKET});
if($h->{DeviceName}) {
require "$attr{global}{modpath}/FHEM/DevIo.pm";
DevIo_CloseDev($h,1);
}
}
### End of copied from Blocking.pm
while(kill 0, $ppid) {
DevIo_OpenDev($hash, 1, "XBMC_ChildExit");
sleep(5);
@@ -105,6 +122,9 @@ sub XBMC_ChildExit($)
sub XBMC_Undefine($$)
{
my ($hash,$arg) = @_;
RemoveInternalTimer($hash);
if($hash->{Protocol} eq 'tcp') {
DevIo_CloseDev($hash);
}
@@ -114,10 +134,62 @@ sub XBMC_Undefine($$)
sub XBMC_Init($)
{
my ($hash) = @_;
#since we just successfully connected to XBMC I guess its safe to assume the device is awake
readingsSingleUpdate($hash,"system","wake",1);
$hash->{LAST_PING} = $hash->{LAST_PONG} = time();
XBMC_Update($hash);
XBMC_QueueCheckConnection($hash);
return undef;
}
sub XBMC_QueueCheckConnection($;$) {
my ($hash, $time) = @_;
# AFAIK when using http this module is not using a persistent TCP connection
if($hash->{Protocol} ne 'http') {
if (!defined($time)) {
$time = AttrVal($hash->{NAME},'pingInterval','60');
}
InternalTimer(time() + $time, "XBMC_CheckConnection", $hash, 0);
}
}
sub XBMC_CheckConnection($) {
my ($hash) = @_;
my $name = $hash->{NAME};
if ($hash->{STATE} eq "disconnected") {
# we are already disconnected
return;
}
#do not call XBMC_CheckConnection a second time before the pong had a chance to arrive
#otherwise the connection will be considered as lost
if ($hash->{LAST_PING} > $hash->{LAST_PONG}) {
Log3 $name, 3, "Last ping (" . $hash->{LAST_PING} . ") is greather than last pong (" . $hash->{LAST_PONG} . ")";
DevIo_Disconnected($hash);
return;
}
my $obj = {
"method" => "JSONRPC.Ping",
};
$obj->{id} = XBMC_CreateId();
$obj->{jsonrpc} = "2.0"; #JSON RPC version has to be passed
# remember: we only get here when using TCP (not HTTP)
my $json = encode_json($obj);
$hash->{LAST_PING} = time();
DevIo_SimpleWrite($hash, $json, 0);
#xbmc seems alive. so keep bugging it
XBMC_QueueCheckConnection($hash);
}
sub XBMC_Update($)
{
my ($hash) = @_;
@@ -143,7 +215,7 @@ sub XBMC_Update($)
}
};
XBMC_Call($hash,$obj,1);
XBMC_PlayerUpdate($hash,0);
XBMC_PlayerUpdate($hash,-1); #-1 -> update all existing players
}
sub XBMC_PlayerUpdate($$)
@@ -170,23 +242,43 @@ sub XBMC_PlayerUpdate($$)
sub XBMC_Read($)
{
my ($hash) = @_;
my $buffer = DevIo_SimpleRead($hash);
return XBMC_ProcessRead($hash, $buffer);
}
sub XBMC_ProcessRead($$)
{
my ($hash, $data) = @_;
my $name = $hash->{NAME};
my $buffer = '';
Log3($name, 5, "XBMC_ProcessRead");
#include previous partial message
if(defined($hash->{PARTIAL}) && $hash->{PARTIAL}) {
$buffer = $hash->{PARTIAL} . DevIo_SimpleRead($hash);
Log3($name, 5, "XBMC_Read: PARTIAL: " . $hash->{PARTIAL});
$buffer = $hash->{PARTIAL};
}
else {
$buffer = DevIo_SimpleRead($hash);
Log3($name, 5, "No PARTIAL buffer");
}
Log3($name, 5, "XBMC_Read: Incoming data: " . $data);
$buffer = $buffer . $data;
Log3($name, 5, "XBMC_Read: Current processing buffer (PARTIAL + incoming data): " . $buffer);
my ($msg,$tail) = XBMC_ParseMsg($buffer);
#processes all complete messages
while($msg) {
my $obj = decode_json($msg);
Log 5, "XBMC received message:" . $msg;
Log3($name, 5, "XBMC_Read: Decoding JSON message. Length: " . length($msg) . " Content: " . $msg);
my $obj = JSON->new->utf8(0)->decode($msg);
#it is a notification if a method name is present
if(defined($obj->{method})) {
XBMC_ProcessNotification($hash,$obj);
}
elsif(defined($obj->{error})) {
Log3($name, 3, "XBMC_Read: Received error message: " . $msg);
}
#otherwise it is a answer of a request
else {
XBMC_ProcessResponse($hash,$obj);
@@ -194,41 +286,14 @@ sub XBMC_Read($)
($msg,$tail) = XBMC_ParseMsg($tail);
}
$hash->{PARTIAL} = $tail;
Log 5, "Tail:" . $tail;
Log3($name, 5, "XBMC_Read: Tail: " . $tail);
Log3($name, 5, "XBMC_Read: PARTIAL: " . $hash->{PARTIAL});
}
sub XBMC_ProcessNotification($$)
sub XBMC_PlayerOnPlay($$)
{
my ($hash,$obj) = @_;
#React on volume change - http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Application.OnVolumeChanged
if($obj->{method} eq "Application.OnVolumeChanged") {
readingsBeginUpdate($hash);
readingsBulkUpdate($hash,'volume',$obj->{params}->{data}->{volume});
readingsBulkUpdate($hash,'mute',($obj->{params}->{data}->{muted} ? 'on' : 'off'));
readingsEndUpdate($hash, 1);
}
#React on play, pause and stop
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnPlay
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnPause
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnStop
elsif($obj->{method} eq "Player.OnPropertyChanged") {
XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
}
elsif($obj->{method} eq "Player.OnSeek") {
#XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
Log 3, "Discard Player.OnSeek event because it is irrelevant";
}
elsif($obj->{method} eq "Player.OnSpeedChanged") {
#XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
Log 3, "Discard Player.OnSpeedChanged event because it is irrelevant";
}
elsif($obj->{method} eq "Player.OnStop") {
readingsSingleUpdate($hash,"playStatus",'stopped',1);
}
elsif($obj->{method} eq "Player.OnPause") {
readingsSingleUpdate($hash,"playStatus",'paused',1);
}
elsif($obj->{method} eq "Player.OnPlay") {
my $name = $hash->{NAME};
my $id = XBMC_CreateId();
my $type = $obj->{params}->{data}->{item}->{type};
if(AttrVal($hash->{NAME},'compatibilityMode','xbmc') eq 'plex' || !defined($obj->{params}->{data}->{item}->{id}) || $type eq "picture" || $type eq "unknown") {
@@ -316,8 +381,55 @@ sub XBMC_ProcessNotification($$)
}
XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
}
sub XBMC_ProcessNotification($$)
{
my ($hash,$obj) = @_;
my $name = $hash->{NAME};
#React on volume change - http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Application.OnVolumeChanged
if($obj->{method} eq "Application.OnVolumeChanged") {
readingsBeginUpdate($hash);
readingsBulkUpdate($hash,'volume',$obj->{params}->{data}->{volume});
readingsBulkUpdate($hash,'mute',($obj->{params}->{data}->{muted} ? 'on' : 'off'));
readingsEndUpdate($hash, 1);
}
#React on play, pause and stop
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnPlay
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnPause
#http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Player.OnStop
elsif($obj->{method} eq "Player.OnPropertyChanged") {
XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
}
elsif($obj->{method} eq "Player.OnSeek") {
#XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
Log3($name, 4, "Discard Player.OnSeek event because it is irrelevant");
}
elsif($obj->{method} eq "Player.OnSpeedChanged") {
#XBMC_PlayerUpdate($hash,$obj->{params}->{data}->{player}->{playerid});
Log3($name, 3, "Discard Player.OnSpeedChanged event because it is irrelevant");
}
elsif($obj->{method} eq "Player.OnStop") {
readingsSingleUpdate($hash,"playStatus",'stopped',1);
}
elsif($obj->{method} eq "Player.OnPause") {
readingsSingleUpdate($hash,"playStatus",'paused',1);
}
elsif($obj->{method} eq "Player.OnPlay") {
XBMC_PlayerOnPlay($hash, $obj);
}
elsif($obj->{method} =~ /(.*).On(.*)/) {
readingsSingleUpdate($hash,lc($1),lc($2),1);
if ((lc($1) eq "system") and (lc($2) eq "sleep")) {
Log3($name, 3, "XBMC notified that it is going to sleep");
#if we immediatlely close our DevIO then fhem will instantly try to reconnect which might
#succeed because XBMC needs a moment to actually shutdown.
#So cancel the current timer, fake that the last pong has arrived ages ago
#and force a connection check in some seconds when we think XBMC actually has shut down
$hash->{LAST_PONG} = 0;
RemoveInternalTimer($hash);
XBMC_QueueCheckConnection($hash, 5);
}
}
return undef;
}
@@ -362,6 +474,7 @@ sub XBMC_ProcessResponse($$)
else {
my $properties = $obj->{result};
if($properties && $properties ne 'OK') {
if ($properties ne 'pong') {
readingsBeginUpdate($hash);
foreach my $key (keys %$properties) {
my $value = $properties->{$key};
@@ -369,6 +482,11 @@ sub XBMC_ProcessResponse($$)
}
readingsEndUpdate($hash, 1);
}
else {
#Pong
$hash->{LAST_PONG} = time();
}
}
}
return undef;
}
@@ -490,6 +608,15 @@ sub XBMC_Set($@)
elsif($cmd eq 'open') {
return XBMC_Set_Open($hash, 'file', @args);
}
elsif($cmd eq 'openmovieid') {
return XBMC_Set_Open($hash, 'movie', @args);
}
elsif($cmd eq 'openepisodeid') {
return XBMC_Set_Open($hash, 'episode', @args);
}
elsif($cmd eq 'addon') {
return XBMC_Set_Addon($hash, @args);
}
elsif($cmd eq 'shuffle') {
return XBMC_Set_Shuffle($hash, @args);
}
@@ -530,6 +657,10 @@ sub XBMC_Set($@)
my $action = $args[0]; #http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Input.Action
return XBMC_Call($hash,{'method' => 'Input.ExecuteAction', 'params' => { 'action' => $action}},0);
}
elsif($cmd eq 'jsonraw') {
my $action = join("",@args);
return XBMC_Call_raw($hash,$action,0);
}
elsif($cmd eq 'showcodec') {
return XBMC_Simple_Call($hash,'Input.ShowCodec');
}
@@ -603,7 +734,7 @@ sub XBMC_Set($@)
my $res = "Unknown argument " . $cmd . ", choose one of " .
"off play:all,audio,video,picture playpause:all,audio,video,picture pause:all,audio,video,picture " .
"prev:all,audio,video,picture next:all,audio,video,picture goto stop:all,audio,video,picture " .
"open opendir shuffle:toggle,on,off repeat:one,all,off volumeUp:noArg volumeDown:noArg " .
"open opendir openmovieid openepisodeid addon shuffle:toggle,on,off repeat:one,all,off volumeUp:noArg volumeDown:noArg " .
"back:noArg contextmenu:noArg down:noArg home:noArg info:noArg left:noArg " .
"right:noArg select:noArg send exec:left,right," .
"up,down,pageup,pagedown,select,highlight,parentdir,parentfolder,back," .
@@ -635,7 +766,7 @@ sub XBMC_Set($@)
"msg " .
"mute:toggle,on,off volume:slider,0,1,100 quit:noArg " .
"eject:noArg hibernate:noArg reboot:noArg shutdown:noArg suspend:noArg " .
"videolibrary:scan,clean audiolibrary:scan,clean statusRequest";
"videolibrary:scan,clean audiolibrary:scan,clean statusRequest jsonraw";
return $res ;
}
@@ -665,6 +796,24 @@ sub XBMC_Set_Open($@)
'directory' => $path
}
};
} elsif($opt eq 'movie') {
$params = {
'item' => {
'movieid' => $path +0
},
'options' => {
'resume' => JSON::true
}
};
} elsif($opt eq 'episode') {
$params = {
'item' => {
'episodeid' => $path +0
},
'options' => {
'resume' => JSON::true
}
};
}
my $obj = {
'method' => 'Player.Open',
@@ -673,6 +822,29 @@ sub XBMC_Set_Open($@)
return XBMC_Call($hash,$obj,0);
}
sub XBMC_Set_Addon($@)
{
my $hash = shift;
my $params;
my $attr = join(" ", @_);
$attr =~ /(".*?"|'.*?'|[^ ]+)[ \t]+(".*?"|'.*?'|[^ ]+)[ \t]+(".*?"|'.*?'|[^ ]+)$/;
my $addonid = $1;
my $paramname = $2;
my $paramvalue = $3;
# printf "$1 $2 $3";
$params = {
'addonid' => $addonid,
'params' => {
$paramname => $paramvalue
}
};
my $obj = {
'method' => 'Addons.ExecuteAddon',
'params' => $params
};
return XBMC_Call($hash,$obj,0);
}
sub XBMC_Set_Message($@)
{
my $hash = shift;
@@ -860,12 +1032,14 @@ sub XBMC_Set_Mute($@)
sub XBMC_Call($$$)
{
my ($hash,$obj,$id) = @_;
my $name = $hash->{NAME};
#add an ID otherwise XBMC will not respond
if($id &&!defined($obj->{id})) {
$obj->{id} = XBMC_CreateId();
}
$obj->{jsonrpc} = "2.0"; #JSON RPC version has to be passed
my $json = encode_json($obj);
Log3($name, 5, "XBMC_Call: Sending: " . $json);
if($hash->{Protocol} eq 'http') {
return XBMC_HTTP_Call($hash,$json,$id);
}
@@ -874,6 +1048,19 @@ sub XBMC_Call($$$)
}
}
sub XBMC_Call_raw($$$)
{
my ($hash,$obj,$id) = @_;
my $name = $hash->{NAME};
Log3($name, 5, "XBMC_Call: Sending: " . $obj);
if($hash->{Protocol} eq 'http') {
return XBMC_HTTP_Call($hash,$obj,$id);
}
else {
return XBMC_TCP_Call($hash,$obj);
}
}
sub XBMC_CreateId()
{
return int(rand(1000000));
@@ -918,7 +1105,7 @@ sub XBMC_HTTP_Call($$$)
if($ret =~ /^error:(\d{3})$/) {
return "HTTP Error Code " . $1;
}
return XBMC_ProcessResponse($hash,decode_json($ret)) if($id);
return XBMC_ProcessResponse($hash,JSON->new->utf8(0)->decode($ret)) if($id);
return undef;
}
@@ -930,7 +1117,7 @@ sub XBMC_HTTP_Request($$@)
my $displayurl= $quiet ? "<hidden>" : $url;
if($url !~ /^(http|https):\/\/([^:\/]+)(:\d+)?(\/.*)$/) {
Log 1, "XBMC_HTTP_Request $displayurl: malformed or unsupported URL";
Log(1, "XBMC_HTTP_Request $displayurl: malformed or unsupported URL");
return undef;
}
@@ -948,7 +1135,7 @@ sub XBMC_HTTP_Request($$@)
if($protocol eq "https") {
eval "use IO::Socket::SSL";
if($@) {
Log 1, $@;
Log(1, $@);
} else {
$conn = IO::Socket::SSL->new(PeerAddr=>"$host:$port", Timeout=>$timeout);
}
@@ -956,7 +1143,7 @@ sub XBMC_HTTP_Request($$@)
$conn = IO::Socket::INET->new(PeerAddr=>"$host:$port", Timeout=>$timeout);
}
if(!$conn) {
Log 1, "XBMC_HTTP_Request $displayurl: Can't connect to $protocol://$host:$port\n";
Log(1, "XBMC_HTTP_Request $displayurl: Can't connect to $protocol://$host:$port\n");
undef $conn;
return undef;
}
@@ -988,7 +1175,7 @@ sub XBMC_HTTP_Request($$@)
vec($rin, $conn->fileno(), 1) = 1;
my $nfound = select($rout=$rin, undef, undef, $timeout);
if($nfound <= 0) {
Log 1, "XBMC_HTTP_Request $displayurl: Select timeout/error: $!";
Log(1, "XBMC_HTTP_Request $displayurl: Select timeout/error: $!");
undef $conn;
return undef;
}
@@ -1001,11 +1188,11 @@ sub XBMC_HTTP_Request($$@)
$ret=~ s/(.*?)\r\n\r\n//s; # Not greedy: switch off the header.
my @header= split("\r\n", $1);
my $hostpath= $quiet ? "<hidden>" : $host . $path;
Log 4, "XBMC_HTTP_Request $displayurl: Got data, length: ".length($ret);
Log(4, "XBMC_HTTP_Request $displayurl: Got data, length: ".length($ret));
if(!length($ret)) {
Log 4, "XBMC_HTTP_Request $displayurl: Zero length data, header follows...";
Log(4, "XBMC_HTTP_Request $displayurl: Zero length data, header follows...");
for (@header) {
Log 4, "XBMC_HTTP_Request $displayurl: $_";
Log(4, "XBMC_HTTP_Request $displayurl: $_");
}
}
undef $conn;
@@ -1099,6 +1286,9 @@ sub XBMC_HTTP_Request($$@)
<li><b>repeat &lt;one|all|off&gt; [&lt;audio|video|picture&gt;]</b> - Sets the repeat mode.</li>
<li><b>open &lt;URI&gt;</b> - Plays the resource located at the URI (can be a url or a file)</li>
<li><b>opendir &lt;path&gt;</b> - Plays the content of the directory</li>
<li><b>openmovieid &lt;path&gt;</b> - Plays the movie of the id</li>
<li><b>openepisodeid &lt;path&gt;</b> - Plays the episode of the id</li>
<li><b>addon &lt;addonid&gt; &lt;parametername&gt; &lt;parametervalue&gt;</b> - Executes addon with one Parameter, for example set xbmc addon script.json-cec command activate</li>
</ul>
<br>Input related commands:<br>
<ul>
@@ -1114,6 +1304,7 @@ sub XBMC_HTTP_Request($$@)
<li><b>showcodec</b> - Shows Codec information</li>
<li><b>exec &lt;action&gt;</b> - Execute an input action. All available actions are listed <a href="http://wiki.xbmc.org/index.php?title=JSON-RPC_API/v6#Input.Action">here</a></li>
<li><b>send &lt;text&gt;</b> - Sends &lt;text&gt; as input to XBMC</li>
<li><b>jsonraw</b> - Sends raw JSON data to XBMC</li>
</ul>
<br>Libary related commands:<br>
<ul>