mod-LGTV_WebOS/82_LGTV_WebOS.pm
2017-02-15 08:52:06 +01:00

1311 lines
42 KiB
Perl

###############################################################################
#
# Developed with Kate
#
# (c) 2017 Copyright: Marko Oldenburg (leongaultier at gmail dot com)
# All rights reserved
#
# This script is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# any later version.
#
# The GNU General Public License can be found at
# http://www.gnu.org/copyleft/gpl.html.
# A copy is found in the textfile GPL.txt and important notices to the license
# from the author is found in LICENSE.txt distributed with these scripts.
#
# This script is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
#
# $Id$
#
###############################################################################
#################################
######### Wichtige Hinweise und Links #################
##
#
################################
package main;
use strict;
use warnings;
use MIME::Base64;
use IO::Socket::INET;
use Digest::SHA qw(sha1_hex);
use JSON qw(decode_json encode_json);
use Encode qw(encode_utf8);
my $version = "0.0.66";
# Declare functions
sub LGTV_WebOS_Initialize($);
sub LGTV_WebOS_Define($$);
sub LGTV_WebOS_Undef($$);
sub LGTV_WebOS_Set($@);
sub LGTV_WebOS_Open($);
sub LGTV_WebOS_Close($);
sub LGTV_WebOS_Read($);
sub LGTV_WebOS_Write($@);
sub LGTV_WebOS_Attr(@);
sub LGTV_WebOS_Handshake($);
sub LGTV_WebOS_ResponseProcessing($$);
sub LGTV_WebOS_Header2Hash($);
sub LGTV_WebOS_Pairing($);
sub LGTV_WebOS_CreateSendCommand($$$;$);
sub LGTV_WebOS_Hybi10Encode($;$$);
sub LGTV_WebOS_PreResponseProsessing($$);
sub LGTV_WebOS_WriteReadings($$);
sub LGTV_WebOS_GetCurrentChannel($);
sub LGTV_WebOS_GetForgroundAppInfo($);
sub LGTV_WebOS_GetAudioStatus($);
sub LGTV_WebOS_TimerStatusRequest($);
sub LGTV_WebOS_GetExternalInputList($);
sub LGTV_WebOS_ProcessRead($$);
sub LGTV_WebOS_ParseMsg($$);
sub LGTV_WebOS_Get3DStatus($);
sub LGTV_WebOS_GetChannelProgramInfo($);
my %lgCommands = (
"getServiceList" => ["ssap://api/getServiceList"],
"getChannelList" => ["ssap://tv/getChannelList"],
"getVolume" => ["ssap://audio/getVolume"],
"getAudioStatus" => ["ssap://audio/getStatus"],
"getCurrentChannel" => ["ssap://tv/getCurrentChannel"],
"getChannelProgramInfo" => ["ssap://tv/getChannelProgramInfo"],
"getForegroundAppInfo" => ["ssap://com.webos.applicationManager/getForegroundAppInfo"],
"getAppList" => ["ssap://com.webos.applicationManager/listApps"],
"getAppStatus" => ["ssap://com.webos.service.appstatus/getAppStatus"],
"getExternalInputList" => ["ssap://tv/getExternalInputList"],
"get3DStatus" => ["ssap://com.webos.service.tv.display/get3DStatus"],
"powerOff" => ["ssap://system/turnOff"],
"powerOn" => ["ssap://system/turnOn"],
"3DOn" => ["ssap://com.webos.service.tv.display/set3DOn"],
"3DOff" => ["ssap://com.webos.service.tv.display/set3DOff"],
"volumeUp" => ["ssap://audio/volumeUp"],
"volumeDown" => ["ssap://audio/volumeDown"],
"channelDown" => ["ssap://tv/channelDown"],
"channelUp" => ["ssap://tv/channelUp"],
"play" => ["ssap://media.controls/play"],
"stop" => ["ssap://media.controls/stop"],
"pause" => ["ssap://media.controls/pause"],
"rewind" => ["ssap://media.controls/rewind"],
"fastForward" => ["ssap://media.controls/fastForward"],
"closeViewer" => ["ssap://media.viewer/close"],
"closeApp" => ["ssap://system.launcher/close"],
"openApp" => ["ssap://system.launcher/open"],
"closeWebApp" => ["ssap://webapp/closeWebApp"],
"openChannel" => ["ssap://tv/openChannel", "channel"],
"openApp" => ["ssap://system.launcher/open"],
"launchApp" => ["ssap://system.launcher/launch", "id"],
"screenMsg" => ["ssap://system.notifications/createToast", "message"],
"mute" => ["ssap://audio/setMute", "mute"],
"volume" => ["ssap://audio/setVolume", "volume"],
"switchInput" => ["ssap://tv/switchInput", "input"],
);
my %openApps = (
'Maxdome' => 'maxdome',
'AmazonVideo' => 'lovefilm.de',
'YouTube' => 'youtube.leanback.v4',
'Netflix' => 'netflix',
'TV' => 'com.webos.app.livetv',
'GooglePlay' => 'googleplaymovieswebos',
'Browser' => 'com.webos.app.browser',
'Chili.tv' => 'Chilieu',
'TVCast' => 'de.2kit.castbrowsing',
'Smartshare' => 'com.webos.app.smartshare',
'Scheduler' => 'com.webos.app.scheduler',
'Miracast' => 'com.webos.app.miracast',
'TVGuide' => 'com.webos.app.tvguide',
'Timemachine' => 'com.webos.app.timemachine',
'ARDMediathek' => 'ard.mediathek',
'Arte' => 'com.3827031.168353',
'WetterMeteo' => 'meteonews',
'Notificationcenter' => 'com.webos.app.notificationcenter'
);
my %openAppsPackageName = (
'maxdome' => 'Maxdome',
'lovefilm.de' => 'AmazonVideo',
'youtube.leanback.v4' => 'YouTube',
'netflix' => 'Netflix',
'com.webos.app.livetv' => 'TV',
'googleplaymovieswebos' => 'GooglePlay',
'com.webos.app.browser' => 'Browser',
'Chilieu' => 'Chili.tv',
'de.2kit.castbrowsing' => 'TVCast',
'com.webos.app.smartshare' => 'Smartshare',
'com.webos.app.scheduler' => 'Scheduler',
'com.webos.app.miracast' => 'Miracast',
'com.webos.app.tvguide' => 'TVGuide',
'com.webos.app.timemachine' => 'Timemachine',
'ard.mediathek' => 'ARDMediathek',
'com.3827031.168353' => 'Arte',
'meteonews' => 'WetterMeteo',
'com.webos.app.notificationcenter' => 'Notificationcenter'
);
sub LGTV_WebOS_Initialize($) {
my ($hash) = @_;
# Provider
$hash->{ReadFn} = "LGTV_WebOS_Read";
$hash->{WriteFn} = "LGTV_WebOS_Write";
# Consumer
$hash->{SetFn} = "LGTV_WebOS_Set";
$hash->{DefFn} = "LGTV_WebOS_Define";
$hash->{UndefFn} = "LGTV_WebOS_Undef";
$hash->{AttrFn} = "LGTV_WebOS_Attr";
$hash->{AttrList} = "disable:1 ".
"channelGuide:1 ".
$readingFnAttributes;
foreach my $d(sort keys %{$modules{LGTV_WebOS}{defptr}}) {
my $hash = $modules{LGTV_WebOS}{defptr}{$d};
$hash->{VERSION} = $version;
}
}
sub LGTV_WebOS_Define($$) {
my ( $hash, $def ) = @_;
my @a = split( "[ \t][ \t]*", $def );
return "too few parameters: define <name> LGTV_WebOS <HOST>" if( @a != 3 );
my $name = $a[0];
my $host = $a[2];
$hash->{HOST} = $host;
$hash->{VERSION} = $version;
$hash->{helper}{channelguide}{counter} = 0;
Log3 $name, 3, "LGTV_WebOS ($name) - defined with host $host";
$attr{$name}{devStateIcon} = 'on:10px-kreis-gruen:off off:10px-kreis-rot:on' if( !defined( $attr{$name}{devStateIcon} ) );
$attr{$name}{room} = 'LGTV' if( !defined( $attr{$name}{room} ) );
readingsSingleUpdate($hash,'state','off', 1);
$modules{LGTV_WebOS}{defptr}{$hash->{HOST}} = $hash;
if( $init_done ) {
LGTV_WebOS_Open($hash);
} else {
InternalTimer( gettimeofday()+15, "LGTV_WebOS_Open", $hash, 0 );
}
return undef;
}
sub LGTV_WebOS_Undef($$) {
my ( $hash, $arg ) = @_;
my $host = $hash->{HOST};
my $name = $hash->{NAME};
RemoveInternalTimer($hash);
LGTV_WebOS_Close($hash);
delete $modules{LGTV_WebOS}{defptr}{$hash->{HOST}};
Log3 $name, 3, "LGTV_WebOS ($name) - device $name deleted";
return undef;
}
sub LGTV_WebOS_Attr(@) {
my ( $cmd, $name, $attrName, $attrVal ) = @_;
my $hash = $defs{$name};
my $orig = $attrVal;
if( $attrName eq "disable" ) {
if( $cmd eq "set" and $attrVal eq "1" ) {
readingsSingleUpdate ( $hash, "state", "disabled", 1 );
$hash->{PARTIAL} = '';
Log3 $name, 3, "LGTV_WebOS ($name) - disabled";
}
elsif( $cmd eq "del" ) {
readingsSingleUpdate ( $hash, "state", "active", 1 );
Log3 $name, 3, "LGTV_WebOS ($name) - enabled";
}
}
if( $attrName eq "disabledForIntervals" ) {
if( $cmd eq "set" ) {
Log3 $name, 3, "LGTV_WebOS ($name) - enable disabledForIntervals";
readingsSingleUpdate ( $hash, "state", "Unknown", 1 );
}
elsif( $cmd eq "del" ) {
readingsSingleUpdate ( $hash, "state", "active", 1 );
Log3 $name, 3, "LGTV_WebOS ($name) - delete disabledForIntervals";
}
}
return undef;
}
sub LGTV_WebOS_TimerStatusRequest($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_TimerStatusRequest');
readingsBeginUpdate($hash);
if( !IsDisabled($name) and $hash->{CD} ) {
Log3 $name, 4, "LGTV_WebOS ($name) - run get functions";
readingsBulkUpdate($hash, 'state', 'on');
readingsBulkUpdate($hash, 'presence', 'present');
if($hash->{helper}{channelguide}{counter} > 5 and AttrVal($name,'channelGuide', 0) == 1 and ReadingsVal($name,'launchApp', 'TV') eq 'TV' ) {
InternalTimer( gettimeofday()+4, 'LGTV_WebOS_GetChannelProgramInfo', $hash, 0 );
$hash->{helper}{channelguide}{counter} = 0;
} else {
LGTV_WebOS_GetAudioStatus($hash);
InternalTimer( gettimeofday()+2, 'LGTV_WebOS_GetCurrentChannel', $hash, 0 ) if( ReadingsVal($name,'launchApp', 'TV') eq 'TV' );
InternalTimer( gettimeofday()+4, 'LGTV_WebOS_GetForgroundAppInfo', $hash, 0 );
InternalTimer( gettimeofday()+6, 'LGTV_WebOS_Get3DStatus', $hash, 0 );
InternalTimer( gettimeofday()+8, 'LGTV_WebOS_GetExternalInputList', $hash, 0 );
}
} elsif( IsDisabled($name) ) {
readingsBulkUpdate($hash, 'state', 'disabled');
} else {
readingsBulkUpdate($hash, 'state', 'off');
readingsBulkUpdate($hash, 'presence', 'absent');
}
readingsEndUpdate($hash, 1);
LGTV_WebOS_Open($hash) if( !IsDisabled($name) and not $hash->{CD} );
$hash->{helper}{channelguide}{counter} = $hash->{helper}{channelguide}{counter} +1;
InternalTimer( gettimeofday()+10,"LGTV_WebOS_TimerStatusRequest", $hash, 1 );
}
sub LGTV_WebOS_Set($@) {
my ($hash, $name, $cmd, @args) = @_;
my ($arg, @params) = @args;
my $uri;
my %payload;
my $inputs;
my @inputs;
if ( defined( $hash->{helper}{device}{inputs} ) and ref( $hash->{helper}{device}{inputs} ) eq "HASH" ) {
@inputs = keys %{ $hash->{helper}{device}{inputs} };
}
@inputs = sort(@inputs);
$inputs = join(",", @inputs);
if($cmd eq 'connect') {
return "usage: connect" if( @args != 0 );
LGTV_WebOS_Open($hash);
return undef;
} elsif($cmd eq 'clearInputList') {
return "usage: clearInputList" if( @args != 0 );
delete $hash->{helper}{device}{inputs};
delete $hash->{helper}{device}{inputapps};
return undef;
} elsif($cmd eq 'pairing') {
return "usage: pairing" if( @args != 0 );
LGTV_WebOS_Pairing($hash);
return undef;
} elsif($cmd eq 'screenMsg') {
return "usage: screenMsg <message>" if( @args < 1 );
my $msg = join(" ", @args);
#$msg =~ s/ä/u/g;
$payload{$lgCommands{$cmd}->[1]} = $msg;
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'on' or $cmd eq 'off') {
return "usage: on/off" if( @args != 0 );
if($cmd eq 'off') {
$uri = $lgCommands{powerOff};
} elsif ($cmd eq 'on') {
$uri = $lgCommands{powerOn};
}
} elsif($cmd eq '3D') {
return "usage: 3D on/off" if( @args != 1 );
if($args[0] eq 'off') {
$uri = $lgCommands{'3DOff'};
} elsif ($args[0] eq 'on') {
$uri = $lgCommands{'3DOn'};
}
} elsif($cmd eq 'mute') {
return "usage: mute" if( @args != 1 );
if($args[0] eq 'off') {
$payload{$lgCommands{$cmd}->[1]} = 'false';
} elsif($args[0] eq 'on') {
$payload{$lgCommands{$cmd}->[1]} = 'true';
}
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'volume') {
return "usage: volume" if( @args != 1 );
$payload{$lgCommands{$cmd}->[1]} = join(" ", @args);
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'launchApp') {
return "usage: launchApp" if( @args != 1 );
$payload{$lgCommands{$cmd}->[1]} = $openApps{join(" ", @args)};
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'input') {
return "usage: input" if( @args != 1 );
my $inputLabel = join(" ", @args);
$payload{$lgCommands{launchApp}->[1]} = $hash->{helper}{device}{inputs}{$inputLabel};
$uri = $lgCommands{launchApp}->[0];
} elsif($cmd eq 'volumeUp') {
return "usage: volumeUp" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'volumeDown') {
return "usage: volumeDown" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'channelDown') {
return "usage: channelDown" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'channelUp') {
return "usage: channelUp" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'openChannel') {
return "usage: openChannel" if( @args != 1 );
$payload{$lgCommands{$cmd}->[1]} = join(" ", @args);
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'getServiceList') {
return "usage: getServiceList" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'getChannelList') {
return "usage: getChannelList" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'getAppList') {
return "usage: getAppList" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'getExternalInputList') {
return "usage: getExternalInputList" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'play') {
return "usage: play" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'stop') {
return "usage: stop" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'fastForward') {
return "usage: fastForward" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'rewind') {
return "usage: rewind" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} elsif($cmd eq 'pause') {
return "usage: pause" if( @args != 0 );
$uri = $lgCommands{$cmd}->[0];
} else {
my $list = "";
$list .= "connect:noArg pairing:noArg screenMsg mute:on,off volume volumeUp:noArg volumeDown:noArg channelDown:noArg channelUp:noArg getServiceList:noArg on:noArg off:noArg launchApp:Maxdome,AmazonVideo,YouTube,Netflix,TV,GooglePlay,Browser,Chilieu,TVCast,Smartshare,Scheduler,Miracast,TVGuide,Timemachine,ARDMediathek,Arte,WetterMeteo,Notificationcenter 3D:on,off stop:noArg play:noArg pause:noArg rewind:noArg fastForward:noArg clearInputList:noArg input:$inputs";
return "Unknown argument $cmd, choose one of $list";
}
LGTV_WebOS_CreateSendCommand($hash,$uri,\%payload);
}
sub LGTV_WebOS_Open($) {
my $hash = shift;
my $name = $hash->{NAME};
my $host = $hash->{HOST};
my $port = 3000;
my $timeout = 0.1;
Log3 $name, 4, "LGTV_WebOS ($name) - Baue Socket Verbindung auf";
my $socket = new IO::Socket::INET ( PeerHost => $host,
PeerPort => $port,
Proto => 'tcp',
Timeout => $timeout
)
or return Log3 $name, 4, "LGTV_WebOS ($name) Couldn't connect to $host:$port"; # open Socket
$hash->{FD} = $socket->fileno();
$hash->{CD} = $socket; # sysread / close won't work on fileno
$selectlist{$name} = $hash;
Log3 $name, 4, "LGTV_WebOS ($name) - Socket Connected";
LGTV_WebOS_Handshake($hash);
Log3 $name, 4, "LGTV_WebOS ($name) - start Handshake";
}
sub LGTV_WebOS_Close($) {
my $hash = shift;
my $name = $hash->{NAME};
return if( !$hash->{CD} );
close($hash->{CD}) if($hash->{CD});
delete($hash->{FD});
delete($hash->{CD});
delete($selectlist{$name});
readingsSingleUpdate($hash, 'state', 'off', 1 );
Log3 $name, 4, "LGTV_WebOS ($name) - Socket Disconnected";
}
sub LGTV_WebOS_Write($@) {
my ($hash,$string) = @_;
my $name = $hash->{NAME};
Log3 $name, 4, "LGTV_WebOS ($name) - WriteFn called";
return Log3 $name, 4, "LGTV_WebOS ($name) - socket not connected"
unless($hash->{CD});
Log3 $name, 4, "LGTV_WebOS ($name) - $string";
syswrite($hash->{CD}, $string);
return undef;
}
sub LGTV_WebOS_Read($) {
my $hash = shift;
my $name = $hash->{NAME};
my $len;
my $buf;
Log3 $name, 4, "LGTV_WebOS ($name) - ReadFn gestartet";
$len = sysread($hash->{CD},$buf,10240); # die genaue Puffergröße wird noch ermittelt
if( !defined($len) or !$len ) {
Log3 $name, 4, "LGTV_WebOS ($name) - connection closed by remote Host";
LGTV_WebOS_Close($hash);
return;
}
unless( defined $buf) {
Log3 $name, 3, "LGTV_WebOS ($name) - Keine Daten empfangen";
return;
}
if( $buf =~ /({"type":".+}}$)/ ) {
$buf =~ /({"type":".+}}$)/;
$buf = $1;
Log3 $name, 5, "LGTV_WebOS ($name) - received correct JSON string, start response processing: $buf";
LGTV_WebOS_ResponseProcessing($hash,$buf);
return;
} elsif( $buf =~ /HTTP\/1.1 101 Switching Protocols/ ) {
Log3 $name, 5, "LGTV_WebOS ($name) - received HTTP data string, start response processing: $buf";
LGTV_WebOS_ResponseProcessing($hash,$buf);
return;
}
Log3 $name, 5, "LGTV_WebOS ($name) - coruppted data found, run LGTV_WebOS_ProcessRead: $buf";
LGTV_WebOS_ProcessRead($hash,$buf);
}
sub LGTV_WebOS_ProcessRead($$) {
my ($hash, $data) = @_;
my $name = $hash->{NAME};
my $buffer = '';
Log3 $name, 4, "LGTV_WebOS ($name) - process read";
if(defined($hash->{PARTIAL}) and $hash->{PARTIAL}) {
Log3 $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL};
$buffer = $hash->{PARTIAL};
} else {
Log3 $name, 4, "LGTV_WebOS ($name) - No PARTIAL buffer";
}
Log3 $name, 5, "LGTV_WebOS ($name) - Incoming data: " . $data;
$buffer = $buffer . $data;
Log3 $name, 5, "LGTV_WebOS ($name) - Current processing buffer (PARTIAL + incoming data): " . $buffer;
my ($json,$tail) = LGTV_WebOS_ParseMsg($hash, $buffer);
while($json) {
$hash->{LAST_RECV} = time();
Log3 $name, 5, "LGTV_WebOS ($name) - Decoding JSON message. Length: " . length($json) . " Content: " . $json;
Log3 $name, 5, "LGTV_WebOS ($name) - Vor Sub: Laenge JSON: " . length($json) . " Content: " . $json . " Tail: " . $tail;
LGTV_WebOS_ResponseProcessing($hash,$json)
unless(not defined($tail) and not ($tail));
($json,$tail) = LGTV_WebOS_ParseMsg($hash, $tail);
Log3 $name, 5, "LGTV_WebOS ($name) - Nach Sub: Laenge JSON: " . length($json) . " Content: " . $json . " Tail: " . $tail;
}
$hash->{PARTIAL} = $tail;
Log3 $name, 5, "LGTV_WebOS ($name) - Tail: " . $tail;
Log3 $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL};
}
sub LGTV_WebOS_Handshake($) {
my $hash = shift;
my $name = $hash->{NAME};
my $host = $hash->{HOST};
my $wsKey = encode_base64(gettimeofday());
my $wsHandshakeCmd = "";
$wsHandshakeCmd .= "GET / HTTP/1.1\r\n";
$wsHandshakeCmd .= "Host: $host\r\n";
$wsHandshakeCmd .= "User-Agent: FHEM\r\n";
$wsHandshakeCmd .= "Upgrade: websocket\r\n";
$wsHandshakeCmd .= "Connection: Upgrade\r\n";
$wsHandshakeCmd .= "Sec-WebSocket-Version: 13\r\n";
$wsHandshakeCmd .= "Sec-WebSocket-Key: " . $wsKey . "\r\n";
LGTV_WebOS_Write($hash,$wsHandshakeCmd);
$hash->{helper}{wsKey} = $wsKey;
Log3 $name, 4, "LGTV_WebOS ($name) - send Handshake to WriteFn";
LGTV_WebOS_TimerStatusRequest($hash);
Log3 $name, 4, "LGTV_WebOS ($name) - start timer status request";
LGTV_WebOS_Pairing($hash);
Log3 $name, 4, "LGTV_WebOS ($name) - start pairing routine";
}
sub LGTV_WebOS_ResponseProcessing($$) {
my ($hash,$response) = @_;
my $name = $hash->{NAME};
########################
### Response has HTML Header
if( $response =~ /HTTP\/1.1 101 Switching Protocols/ ) {
my $data = $response;
my $header = LGTV_WebOS_Header2Hash($data);
################################
### Handshake for first Connect
if( defined($header->{'Sec-WebSocket-Accept'})) {
my $keyAccept = $header->{'Sec-WebSocket-Accept'};
Log3 $name, 5, "LGTV_WebOS ($name) - keyAccept: $keyAccept";
my $wsKey = $hash->{helper}{wsKey};
my $expectedResponse = trim(encode_base64(pack('H*', sha1_hex(trim($wsKey)."258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))));
if ($keyAccept eq $expectedResponse) {
Log3 $name, 3, "LGTV_WebOS ($name) - Sucessfull WS connection to $hash->{HOST}";
readingsSingleUpdate($hash, 'state', 'on', 1 );
} else {
LGTV_WebOS_Close($hash);
Log3 $name, 3, "LGTV_WebOS ($name) - ERROR: Unsucessfull WS connection to $hash->{HOST}";
}
}
return undef;
}
elsif( $response =~ m/^{"type":".+}}$/ ) {
Log3 $name, 4, "LGTV_WebOS ($name) - JSON detected, run LGTV_WebOS_WriteReadings";
#my $json = LGTV_WebOS_PreResponseProsessing($hash,$response);
#my $json = LGTV_WebOS_ProcessRead($hash,$response);
my $json = $response;
Log3 $name, 4, "LGTV_WebOS ($name) - Corrected JSON String: $json" if($json);
if(not defined($json) or not ($json) ) {
Log3 $name, 4, "LGTV_WebOS ($name) - Corrected JSON String empty";
return;
}
my $decode_json = decode_json(encode_utf8($json));
LGTV_WebOS_WriteReadings($hash,$decode_json);
return undef;
}
Log3 $name, 4, "LGTV_WebOS ($name) - no Match found";
}
sub LGTV_WebOS_WriteReadings($$) {
my ($hash,$decode_json) = @_;
my $name = $hash->{NAME};
my $mute;
my $response;
Log3 $name, 4, "LGTV_WebOS ($name) - Beginn Readings writing";
readingsBeginUpdate($hash);
if( ref($decode_json->{payload}{services}) eq "ARRAY" and scalar(@{$decode_json->{payload}{services}}) > 0 ) {
foreach my $services (@{$decode_json->{payload}{services}}) {
readingsBulkUpdate($hash,'service_'.$services->{name},'v.'.$services->{version});
}
}
elsif( ref($decode_json->{payload}{devices}) eq "ARRAY" and scalar(@{$decode_json->{payload}{devices}}) > 0 ) {
foreach my $devices ( @{$decode_json->{payload}{devices}} ) {
if( not defined($hash->{helper}{device}{inputs}{$devices->{label}}) or not defined($hash->{helper}{device}{inputapps}{$devices->{appId}}) ) {
$hash->{helper}{device}{inputs}{$devices->{label}} = $devices->{appId};
$hash->{helper}{device}{inputapps}{$devices->{appId}} = $devices->{label};
}
readingsBulkUpdate($hash,'extInput_'.$devices->{label},'connect_'.$devices->{connected});
}
}
elsif( ref($decode_json->{payload}{programList}) eq "ARRAY" and scalar(@{$decode_json->{payload}{programList}}) > 0 ) {
my $count = 0;
foreach my $programList ( @{$decode_json->{payload}{programList}} ) {
readingsBulkUpdate($hash,'channelCurrentTitle',$programList->{programName}) if($count < 1);
readingsBulkUpdate($hash,'channelNextTitle',$programList->{programName}) if($count < 2);
$count++;
return if($count > 1);
}
}
elsif( defined($decode_json->{payload}{'mute'}) or defined($decode_json->{payload}{'muted'})) {
if( defined($decode_json->{payload}{'mute'}) and $decode_json->{payload}{'mute'} eq 'true' ) {
readingsBulkUpdate($hash,'mute','on');
} elsif( defined($decode_json->{payload}{'mute'}) ) {
if( $decode_json->{payload}{'mute'} eq 'false' ) {
readingsBulkUpdate($hash,'mute','off');
}
}
if( defined($decode_json->{payload}{'muted'}) and $decode_json->{payload}{'muted'} eq 'true' ) {
readingsBulkUpdate($hash,'mute','on');
} elsif( defined($decode_json->{payload}{'muted'}) and $decode_json->{payload}{'muted'} eq 'false' ) {
readingsBulkUpdate($hash,'mute','off');
}
}
elsif( defined($decode_json->{payload}{status3D}{status}) ) {
if( $decode_json->{payload}{status3D}{status} eq 'false' ) {
readingsBulkUpdate($hash,'3D','off');
} elsif( $decode_json->{payload}{status3D}{status} eq 'true' ) {
readingsBulkUpdate($hash,'3D','on');
}
readingsBulkUpdate($hash,'3DMode',$decode_json->{payload}{status3D}{pattern});
}
elsif( defined($decode_json->{payload}{appId}) ) {
if( $decode_json->{payload}{appId} =~ /com.webos.app.externalinput/ or $decode_json->{payload}{appId} =~ /com.webos.app.hdmi/ ) {
readingsBulkUpdate($hash,'input',$hash->{helper}{device}{inputapps}{$decode_json->{payload}{appId}});
readingsBulkUpdate($hash,'launchApp','-');
} else {
readingsBulkUpdate($hash,'launchApp',$openAppsPackageName{$decode_json->{payload}{appId}});
readingsBulkUpdate($hash,'input','-');
}
}
if( defined($decode_json->{type}) ) {
if( ($decode_json->{type} eq 'response' and $decode_json->{payload}{returnValue} eq 'true') or ($decode_json->{type} eq 'registered') and defined($decode_json->{payload}{'client-key'}) ) {
$response = 'ok';
readingsBulkUpdate($hash,'pairing','paired');
} elsif( $decode_json->{type} eq 'error' ) {
$response = "error - $decode_json->{error}";
if($decode_json->{error} eq '401 insufficient permissions' or $decode_json->{error} eq '401 insufficient permissions (not registered)') {
readingsBulkUpdate($hash,'pairing','unpaired');
}
}
}
readingsBulkUpdate($hash,'lgKey',$decode_json->{payload}{'client-key'});
readingsBulkUpdate($hash,'volume',$decode_json->{payload}{'volume'});
readingsBulkUpdate($hash,'lastResponse',$response);
if( ReadingsVal($name,'launchApp','none') eq 'TV') {
readingsBulkUpdate($hash,'channelId',$decode_json->{payload}{'channelNumber'});
readingsBulkUpdate($hash,'channel',$decode_json->{payload}{'channelName'});
readingsBulkUpdate($hash,'channelMedia',$decode_json->{payload}{'channelTypeName'});
} else {
readingsBulkUpdate($hash,'channelId','-');
readingsBulkUpdate($hash,'channel','-');
readingsBulkUpdate($hash,'channelMedia','-');
readingsBulkUpdate($hash,'channelCurrentTitle','-');
readingsBulkUpdate($hash,'channelNextTitle','-');
}
readingsEndUpdate($hash, 1);
}
sub LGTV_WebOS_Pairing($) {
my $hash = shift;
my $name = $hash->{NAME};
my $lgKey;
Log3 $name, 4, "LGTV_WebOS ($name) - HASH handshakePayload";
my %handshakePayload = ( "pairingType" => "PROMPT",
"manifest" => {
"manifestVersion" => 1,
"appVersion" => "1.1",
"signed" => {
"created" => "20161123",
"appId" => "com.lge.test",
"vendorId" => "com.lge",
"localizedAppNames" => {
"" => "FHEM LG Remote",
"de-DE" => "FHEM LG Fernbedinung"
},
"localizedVendorNames" => {
"" => "LG Electronics"
},
"permissions" => [
"TEST_SECURE",
"CONTROL_INPUT_TEXT",
"CONTROL_MOUSE_AND_KEYBOARD",
"READ_INSTALLED_APPS",
"READ_LGE_SDX",
"READ_NOTIFICATIONS",
"SEARCH",
"WRITE_SETTINGS",
"WRITE_NOTIFICATION_ALERT",
"CONTROL_POWER",
"READ_CURRENT_CHANNEL",
"READ_RUNNING_APPS",
"READ_UPDATE_INFO",
"UPDATE_FROM_REMOTE_APP",
"READ_LGE_TV_INPUT_EVENTS",
"READ_TV_CURRENT_TIME"
],
"serial" => "2f930e2d2cfe083771f68e4fe7bb07"
},
"permissions" => [
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE",
"TEST_OPEN",
"TEST_PROTECTED",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_RECORDING",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_POWER",
"READ_APP_STATUS",
"READ_CURRENT_CHANNEL",
"READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE",
"READ_RUNNING_APPS",
"READ_TV_CHANNEL_LIST",
"WRITE_NOTIFICATION_TOAST",
"READ_POWER_STATE",
"READ_COUNTRY_INFO"
],
"signatures" => [
{
"signatureVersion" => 1,
"signature" => "eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="
}
]
}
);
my $usedHandshake = \%handshakePayload;
my $key = ReadingsVal($name, 'lgKey', '');
$usedHandshake->{'client-key'} = $key if( defined($key));
LGTV_WebOS_CreateSendCommand($hash, undef, $usedHandshake, 'register');
Log3 $name, 4, "LGTV_WebOS ($name) - Send pairing informations";
}
sub LGTV_WebOS_CreateSendCommand($$$;$) {
my ($hash, $uri, $payload, $type) = @_;
my $name = $hash->{NAME};
my $err;
$type = 'request' if( not defined($type) );
my $command = {};
$command->{'client-key'} = ReadingsVal($name, 'lgKey', '') if( $type ne 'register' );
$command->{id} = $type."_".gettimeofday();
$command->{type} = $type;
$command->{uri} = $uri if($uri);
$command->{payload} = $payload if( defined($payload) );
#Log3 $name, 5, "LGTV_WebOS ($name) - Payload Message: $command->{payload}{message}";
my $cmd = encode_json($command);
Log3 $name, 5, "LGTV_WebOS ($name) - Sending command: $cmd";
LGTV_WebOS_Write($hash, LGTV_WebOS_Hybi10Encode($cmd, "text", 1));
return undef;
}
sub LGTV_WebOS_Hybi10Encode($;$$) {
my ($payload, $type, $masked) = @_;
$type //= "text";
$masked //= 1;
my @frameHead;
my $frame = "";
my $payloadLength = length($payload);
if ($type eq "text") {
# first byte indicates FIN, Text-Frame (10000001):
$frameHead[0] = 129;
} elsif ($type eq "close") {
# first byte indicates FIN, Close Frame(10001000):
$frameHead[0] = 136;
} elsif ($type eq "ping") {
# first byte indicates FIN, Ping frame (10001001):
$frameHead[0] = 137;
} elsif ($type eq "pong") {
# first byte indicates FIN, Pong frame (10001010):
$frameHead[0] = 138;
}
# set mask and payload length (using 1, 3 or 9 bytes)
if ($payloadLength > 65535) {
# TODO
my $payloadLengthBin = sprintf('%064b', $payloadLength);
$frameHead[1] = ($masked) ? 255 : 127;
for (my $i = 0; $i < 8; $i++) {
$frameHead[$i + 2] = oct("0b".substr($payloadLengthBin, $i*8, $i*8+8));
}
# most significant bit MUST be 0 (close connection if frame too big)
if ($frameHead[2] > 127) {
#$this->close(1004);
return undef;
}
} elsif ($payloadLength > 125) {
my $payloadLengthBin = sprintf('%016b', $payloadLength);
$frameHead[1] = ($masked) ? 254 : 126;
$frameHead[2] = oct("0b".substr($payloadLengthBin, 0, 8));
$frameHead[3] = oct("0b".substr($payloadLengthBin, 8, 16));
} else {
$frameHead[1] = ($masked) ? $payloadLength + 128 : $payloadLength;
}
# convert frame-head to string:
for (my $i = 0; $i < scalar(@frameHead); $i++) {
$frameHead[$i] = chr($frameHead[$i]);
}
my @mask;
if ($masked) {
# generate a random mask:
for (my $i = 0; $i < 4; $i++) {
#$mask[$i] = chr(int(rand(255)));
$mask[$i] = chr(int(25*$i));
}
@frameHead = (@frameHead, @mask);
}
$frame = join("", @frameHead);
# append payload to frame:
my $char;
for (my $i = 0; $i < $payloadLength; $i++) {
$char = substr($payload, $i, 1);
$frame .= ($masked) ? $char ^ $mask[$i % 4] : $char;
}
return $frame;
}
sub LGTV_WebOS_GetAudioStatus($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_GetAudioStatus');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{getAudioStatus},undef);
}
sub LGTV_WebOS_GetCurrentChannel($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_GetCurrentChannel');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{getCurrentChannel},undef);
}
sub LGTV_WebOS_GetForgroundAppInfo($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_GetForgroundAppInfo');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{getForegroundAppInfo},undef);
}
sub LGTV_WebOS_GetExternalInputList($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_GetExternalInputList');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{getExternalInputList},undef);
}
sub LGTV_WebOS_Get3DStatus($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_Get3DStatus');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{get3DStatus},undef);
}
sub LGTV_WebOS_GetChannelProgramInfo($) {
my $hash = shift;
my $name = $hash->{NAME};
RemoveInternalTimer($hash,'LGTV_WebOS_GetChannelProgramInfo');
LGTV_WebOS_CreateSendCommand($hash,$lgCommands{getChannelProgramInfo},undef);
}
#############################################
### my little Helper
sub LGTV_WebOS_ParseMsg($$) {
my ($hash, $buffer) = @_;
my $name = $hash->{NAME};
my $open = 0;
my $close = 0;
my $msg = '';
my $tail = '';
if($buffer) {
foreach my $c (split //, $buffer) {
if($open == $close && $open > 0) {
$tail .= $c;
Log3 $name, 5, "LGTV_WebOS ($name) - $open == $close && $open > 0";
} elsif(($open == $close) && ($c ne '{')) {
Log3 $name, 5, "LGTV_WebOS ($name) - Garbage character before message: " . $c;
} else {
if($c eq '{') {
$open++;
} elsif($c eq '}') {
$close++;
}
$msg .= $c;
}
}
if($open != $close) {
$tail = $msg;
$msg = '';
}
}
Log3 $name, 5, "LGTV_WebOS ($name) - return msg: $msg and tail: $tail";
return ($msg,$tail);
}
sub LGTV_WebOS_Header2Hash($) {
my ( $string ) = @_;
my %hash = ();
foreach my $line (split("\r\n", $string)) {
my ($key,$value) = split( ": ", $line );
next if( !$value );
$value =~ s/^ //;
$hash{$key} = $value;
}
return \%hash;
}
sub LGTV_WebOS_PreResponseProsessing($$) {
my ($hash,$json) = @_;
my $name = $hash->{NAME};
Log3 $name, 4, "LGTV_WebOS ($name) - pre processing response data";
my $len = length($json);
my @letterArray = split("",$json);
my $letter = "";
my $count = 0;
my $marker = 0;
my $corrected_json;
for(my $i = 0; $i < $len; $i++) {
$marker = 1 if($count > 0);
$letter = $letterArray[0];
$count++ if($letter eq '{');
$count-- if($letter eq '}');
$corrected_json .= $letter if($count > 0);
if( $count == 0 and $marker == 1 ) {
$marker = 0;
$corrected_json .= '}';
return $corrected_json;
}
shift(@letterArray);
}
}
1;