diff --git a/fhem/CHANGED b/fhem/CHANGED index b695fd3c5..055d04d80 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 82_LGTV_WebOS: split in to packages - bugfix: 93_DbLog: $data{firstvalX} now usable, forum:#126631 - change: 70_DENON_AVR: applied STATE changes (thx Shadow3561) - change: 73_AutoShuttersControl: change ShuttersInformation fn diff --git a/fhem/FHEM/82_LGTV_WebOS.pm b/fhem/FHEM/82_LGTV_WebOS.pm index 469142104..e4911f4c7 100644 --- a/fhem/FHEM/82_LGTV_WebOS.pm +++ b/fhem/FHEM/82_LGTV_WebOS.pm @@ -28,184 +28,37 @@ # $Id$ # ############################################################################### - -################################# -######### Wichtige Hinweise und Links ################# -# -## Das JSON Modul immer in einem eval aufrufen -# $data = eval{decode_json($data)}; -# -## -## -## -# - -################################ - -package main; +package FHEM::LGTV_WebOS; use strict; use warnings; -use experimental qw /switch/; + +require FHEM::Devices::LGTV::LGTVWebOS; use FHEM::Meta; -my $missingModul = ""; +use GPUtils qw(GP_Import); -eval { require MIME::Base64; 1 } or $missingModul .= "MIME::Base64 "; -eval { require IO::Socket::INET; 1 } or $missingModul .= "IO::Socket::INET "; +#-- Run before package compilation +BEGIN { + #-- Export to main context with different name + GP_Import(qw( readingFnAttributes)); +} -## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements)) -eval { use Digest::SHA qw /sha1_hex/; 1 } or $missingModul .= "Digest::SHA "; -eval { use Encode qw /encode_utf8 decode_utf8/; 1 } - or $missingModul .= "Encode "; -## use critic +sub ::LGTV_WebOS_Initialize { goto &Initialize } -eval { require Blocking; 1 } or $missingModul .= "Blocking "; - -# try to use JSON::MaybeXS wrapper -# for chance of better performance + open code -eval { - require JSON::MaybeXS; - import JSON::MaybeXS qw( decode_json encode_json ); - 1; -} or do { - - # try to use JSON wrapper - # for chance of better performance - eval { - # JSON preference order - local $ENV{PERL_JSON_BACKEND} = - 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' - unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); - - require JSON; - import JSON qw( decode_json encode_json ); - 1; - } or do { - - # In rare cases, Cpanel::JSON::XS may - # be installed but JSON|JSON::MaybeXS not ... - eval { - require Cpanel::JSON::XS; - import Cpanel::JSON::XS qw(decode_json encode_json); - 1; - } or do { - - # In rare cases, JSON::XS may - # be installed but JSON not ... - eval { - require JSON::XS; - import JSON::XS qw(decode_json encode_json); - 1; - } or do { - - # Fallback to built-in JSON which SHOULD - # be available since 5.014 ... - eval { - require JSON::PP; - import JSON::PP qw(decode_json encode_json); - 1; - } or do { - - # Fallback to JSON::backportPP in really rare cases - require JSON::backportPP; - import JSON::backportPP qw(decode_json encode_json); - 1; - }; - }; - }; - }; -}; - -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", "channelNumber" ], - "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', - 'AmazonLovefilm' => 'lovefilm.de', - 'AmazonVideo' => 'amazon', - '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', - 'Plex' => 'cdp-30', - 'SkyOnline' => 'de.sky.skyonline', - 'Smart-IPTV' => 'com.1827622.109556', - 'Spotify' => 'spotify-beehive', - 'DuplexIPTV' => 'com.duplexiptv.app', - 'Disney+' => 'com.disney.disneyplus-prod', - 'Smart-IPTV' => 'siptv', - 'AppleTV' => 'com.apple.appletv', - 'Joyn' => 'joyn', - 'YouTube-Kids' => 'youtube.leanback.kids.v4', - 'DAZN' => 'dazn', - 'SkyQ' => 'com.skygo.app.de.q', - 'WaipuTv' => 'tv.waipu.app.waipu-lg', -); - -my %openAppsPackageName = reverse %openApps; - -sub LGTV_WebOS_Initialize { - my ($hash) = @_; +sub Initialize { + my $hash = shift; # Provider - $hash->{ReadFn} = "LGTV_WebOS_Read"; - $hash->{WriteFn} = "LGTV_WebOS_Write"; + $hash->{ReadFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Read; + $hash->{WriteFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Write; # Consumer - $hash->{SetFn} = "LGTV_WebOS_Set"; - $hash->{DefFn} = "LGTV_WebOS_Define"; - $hash->{UndefFn} = "LGTV_WebOS_Undef"; - $hash->{AttrFn} = "LGTV_WebOS_Attr"; + $hash->{SetFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Set; + $hash->{DefFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Define; + $hash->{UndefFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Undef; + $hash->{AttrFn} = \&FHEM::Devices::LGTV::LGTVWebOS::Attr; $hash->{AttrList} = "disable:1 " . "channelGuide:1 " @@ -219,1610 +72,10 @@ sub LGTV_WebOS_Initialize { return FHEM::Meta::InitMod( __FILE__, $hash ); } -sub LGTV_WebOS_Define { - my ( $hash, $def ) = @_; - my $version; - - return $@ unless ( FHEM::Meta::SetInternals($hash) ); - - $version = FHEM::Meta::Get( $hash, 'version' ); - our $VERSION = $version; - - my @a = split( "[ \t][ \t]*", $def ); - - return "too few parameters: define LGTV_WebOS " if ( @a != 3 ); - return - "Cannot define LGTV_WebOS device. Perl modul ${missingModul} is missing." - if ($missingModul); - - my $name = $a[0]; - my $host = $a[2]; - - $hash->{HOST} = $host; - $hash->{VERSION} = version->parse($VERSION)->normal; - $hash->{PARTIAL} = ''; - $hash->{helper}{device}{channelguide}{counter} = 0; - $hash->{helper}{device}{registered} = 0; - $hash->{helper}{device}{runsetcmd} = 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} ) ); - CommandDeleteReading( undef, $name . ' presence' ) - if ( AttrVal( $name, 'pingPresence', 0 ) == 0 ); - - $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} } = $hash; - - if ($init_done) { - LGTV_WebOS_TimerStatusRequest($hash); - } - else { - InternalTimer( gettimeofday() + 15, - "LGTV_WebOS_TimerStatusRequest", $hash ); - } - - $hash->{helper}->{lastResponse} = - int( gettimeofday() ); # Check Socket KeepAlive - - return; -} - -sub LGTV_WebOS_Undef { - my ( $hash, $arg ) = @_; - - my $host = $hash->{HOST}; - my $name = $hash->{NAME}; - - RemoveInternalTimer($hash); - delete $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} }; - Log3( $name, 3, "LGTV_WebOS ($name) - device $name deleted" ); - - return; -} - -sub LGTV_WebOS_Attr { - my ( $cmd, $name, $attrName, $attrVal ) = @_; - my $hash = $defs{$name}; - - my $orig = $attrVal; - - if ( $attrName eq "disable" ) { - if ( $cmd eq "set" && $attrVal eq "1" ) { - RemoveInternalTimer($hash); - 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" ); - LGTV_WebOS_TimerStatusRequest($hash); - } - } - - 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; -} - -sub LGTV_WebOS_TimerStatusRequest { - my $hash = shift; - my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash, 'LGTV_WebOS_TimerStatusRequest' ); - - readingsBeginUpdate($hash); - - if ( !IsDisabled($name) - && $hash->{CD} - && $hash->{helper}{device}{registered} == 1 ) - { - - Log3( $name, 4, "LGTV_WebOS ($name) - run get functions" ); - - LGTV_WebOS_Presence($hash) - if ( AttrVal( $name, 'pingPresence', 0 ) == 1 ); - - if ( $hash->{helper}{device}{channelguide}{counter} > 2 - && AttrVal( $name, 'channelGuide', 0 ) == 1 - && ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' ) - { - - LGTV_WebOS_GetChannelProgramInfo($hash); - $hash->{helper}{device}{channelguide}{counter} = 0; - - } - else { - - LGTV_WebOS_GetAudioStatus($hash); - InternalTimer( gettimeofday() + 2, - 'LGTV_WebOS_GetCurrentChannel', $hash ) - if ( ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' ); - InternalTimer( gettimeofday() + 4, - 'LGTV_WebOS_GetForgroundAppInfo', $hash ); - InternalTimer( gettimeofday() + 6, 'LGTV_WebOS_Get3DStatus', - $hash ); - InternalTimer( gettimeofday() + 8, - 'LGTV_WebOS_GetExternalInputList', $hash ); - } - - } - elsif ( IsDisabled($name) ) { - - LGTV_WebOS_Close($hash); - LGTV_WebOS_Presence($hash) - if ( AttrVal( $name, 'pingPresence', 0 ) == 1 ); - $hash->{helper}{device}{runsetcmd} = 0; - readingsBulkUpdateIfChanged( $hash, 'state', 'disabled' ); - - } - else { - - LGTV_WebOS_Presence($hash) - if ( AttrVal( $name, 'pingPresence', 0 ) == 1 ); - - readingsBulkUpdateIfChanged( $hash, 'channel', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelName', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelMedia', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime', '-' ); - - $hash->{helper}{device}{runsetcmd} = 0; - } - - readingsEndUpdate( $hash, 1 ); - - LGTV_WebOS_Open($hash) if ( !IsDisabled($name) && !$hash->{CD} ); - - $hash->{helper}{device}{channelguide}{counter} = - $hash->{helper}{device}{channelguide}{counter} + 1; - InternalTimer( gettimeofday() + 10, "LGTV_WebOS_TimerStatusRequest", - $hash ); - - LGTV_WebOS_SocketKeepAlive($hash) - if ( AttrVal( $name, 'keepAliveCheckTime', 0 ) > 0 ) - ; # Check Socket KeepAlive - - return; -} - -sub LGTV_WebOS_Set -{ ## no critic (Subroutine "LGTV_WebOS_Set" with high complexity score) - my ( $hash, $name, $cmd, @args ) = @_; - my ( $arg, @params ) = @args; - - my $uri; - my %payload; - - given ($cmd) { - when ('connect') { - return "usage: connect" if ( @args != 0 ); - - LGTV_WebOS_Open($hash); - - return; - - } - when ('clearInputList') { - return "usage: clearInputList" if ( @args != 0 ); - - delete $hash->{helper}{device}{inputs}; - delete $hash->{helper}{device}{inputapps}; - - return; - - } - when ('pairing') { - return "usage: pairing" if ( @args != 0 ); - - LGTV_WebOS_Pairing($hash); - - return; - - } - when ('screenMsg') { - return "usage: screenMsg " if ( @args < 1 ); - - my $msg = join( " ", @args ); - $payload{ $lgCommands{$cmd}->[1] } = decode_utf8($msg); - $uri = $lgCommands{$cmd}->[0]; - - } - when ('off') { - return "usage: on/off" if ( @args != 0 ); - - $uri = $lgCommands{powerOff}; - } - when ('on') { - if ( AttrVal( $name, 'wakeOnLanMAC', 'none' ) ne 'none' ) { - LGTV_WebOS_WakeUp_Udp( - $hash, - AttrVal( $name, 'wakeOnLanMAC', 0 ), - AttrVal( $name, 'wakeOnLanBroadcast', '255.255.255.255' ) - ); - return; - } - elsif ( AttrVal( $name, 'wakeupCmd', 'none' ) ne 'none' ) { - my $wakeupCmd = AttrVal( $name, 'wakeupCmd', 'none' ); - if ( $wakeupCmd =~ s/^[ \t]*\{|\}[ \t]*$//xg ) { - Log3( $name, 4, -"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd" - ); - eval { $wakeupCmd } or do { - Log3( $name, 2, -"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd failed" - ); - return; - }; - return; - } - else { - Log3( $name, 4, -"LGTV_WebOS executing wake-up command (fhem): $wakeupCmd" - ); - fhem $wakeupCmd; - return; - } - } - else { - $uri = $lgCommands{powerOn}; - } - } - when ('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'}; - } - - } - when ('mute') { - return "usage: mute" if ( @args != 1 ); - - if ( $args[0] eq 'off' ) { - - $uri = $lgCommands{volumeDown}->[0]; - - } - elsif ( $args[0] eq 'on' ) { - - $payload{ $lgCommands{$cmd}->[1] } = 'true'; - $uri = $lgCommands{$cmd}->[0]; - } - - } - when ('volume') { - return "usage: volume" if ( @args != 1 ); - - $payload{ $lgCommands{$cmd}->[1] } = int( join( " ", @args ) ); - $uri = $lgCommands{$cmd}->[0]; - - } - when ('launchApp') { - return "usage: launchApp" if ( @args != 1 ); - - $payload{ $lgCommands{$cmd}->[1] } = - $openApps{ join( " ", @args ) }; - $uri = $lgCommands{$cmd}->[0]; - - } - when ('input') { - return "usage: input" if ( @args != 1 ); - - my $inputLabel = join( " ", @args ); - $payload{ $lgCommands{launchApp}->[1] } = - $hash->{helper}{device}{inputs}{$inputLabel}; - $uri = $lgCommands{launchApp}->[0]; - - } - when ('volumeUp') { - return "usage: volumeUp" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('volumeDown') { - return "usage: volumeDown" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('channelDown') { - return "usage: channelDown" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('channelUp') { - return "usage: channelUp" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('channel') { - return "usage: channel" if ( @args != 1 ); - - $payload{ $lgCommands{openChannel}->[1] } = join( " ", @args ); - $uri = $lgCommands{openChannel}->[0]; - - } - when ('getServiceList') { - return "usage: getServiceList" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('getChannelList') { - return "usage: getChannelList" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('getAppList') { - return "usage: getAppList" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('getExternalInputList') { - return "usage: getExternalInputList" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('play') { - return "usage: play" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('stop') { - return "usage: stop" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('fastForward') { - return "usage: fastForward" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('rewind') { - return "usage: rewind" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - when ('pause') { - return "usage: pause" if ( @args != 0 ); - - $uri = $lgCommands{$cmd}->[0]; - - } - default { - my $list = ""; - $list .= -'connect:noArg pairing:noArg screenMsg mute:on,off volume:slider,0,1,100 volumeUp:noArg volumeDown:noArg channelDown:noArg channelUp:noArg getServiceList:noArg on:noArg off:noArg'; - $list .= -' 3D:on,off stop:noArg play:noArg pause:noArg rewind:noArg fastForward:noArg clearInputList:noArg channel'; - $list .= - ## no critic (Expression form of map. See page 169 of PBP) - ' launchApp:' . join( ',', => map qq{$_} => keys %openApps ); - $list .= ' input:' . join( - ',', - ## no critic (Expression form of map. See page 169 of PBP) - => map qq{$_} => keys %{ $hash->{helper}{device}{inputs} } - ) - if ( defined( $hash->{helper}{device}{inputs} ) - && ref( $hash->{helper}{device}{inputs} ) eq "HASH" ); - - return "Unknown argument $cmd, choose one of $list"; - } - } - - $hash->{helper}{device}{runsetcmd} = $hash->{helper}{device}{runsetcmd} + 1; - return 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 = IO::Socket::INET->new( - PeerHost => $host, - PeerPort => $port, - Proto => 'tcp', - KeepAlive => 1, - 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; - - $hash->{helper}->{lastResponse} = - int( gettimeofday() ); # Check Socket KeepAlive - - Log3( $name, 4, "LGTV_WebOS ($name) - Socket Connected" ); - - LGTV_WebOS_Handshake($hash); - Log3( $name, 4, "LGTV_WebOS ($name) - start Handshake" ); - - return; -} - -sub LGTV_WebOS_Close { - my $hash = shift; - my $name = $hash->{NAME}; - - return if ( !$hash->{CD} ); - - delete( $hash->{PARTIAL} ); - - close( $hash->{CD} ) if ( $hash->{CD} ); - delete( $hash->{CD} ); - - delete( $selectlist{$name} ); - delete( $hash->{FD} ); - - readingsSingleUpdate( $hash, 'state', 'off', 1 ); - - Log3( $name, 4, "LGTV_WebOS ($name) - Socket Disconnected" ); - - return; -} - -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; -} - -sub LGTV_WebOS_SocketKeepAlive { - my $hash = shift; - my $name = $hash->{NAME}; - - if ( - int( gettimeofday() ) - int( $hash->{helper}->{lastResponse} ) > - AttrVal( $name, 'keepAliveCheckTime', 0 ) ) - { - LGTV_WebOS_SocketClosePresenceAbsent( $hash, 'absent' ); - Log3( $name, 4, -"LGTV_WebOS ($name) - KeepAlive It looks like there no Data more response" - ); - } - - return; -} - -sub LGTV_WebOS_Read { - my $hash = shift; - my $name = $hash->{NAME}; - - my $len; - my $buf; - - Log3( $name, 4, "LGTV_WebOS ($name) - ReadFn started" ); - - $hash->{helper}->{lastResponse} = - int( gettimeofday() ); # Check Socket KeepAlive - $len = sysread( $hash->{CD}, $buf, 10240 ); - - if ( !defined($len) || !$len ) { - - LGTV_WebOS_Close($hash); - - return; - } - - unless ( defined $buf ) { - Log3( $name, 3, "LGTV_WebOS ($name) - no data received" ); - return; - } - - if ( $buf =~ /(\{"type":".+}}$)/x ) { - - $buf =~ /(\{"type":".+}}$)/x; - ## no critic (Capture variable used outside conditional. See page 253 of PBP) - $buf = $1; - ## use critic - - Log3( $name, 4, -"LGTV_WebOS ($name) - received correct JSON string, start response processing: $buf" - ); - LGTV_WebOS_ResponseProcessing( $hash, $buf ); - - } - elsif ( $buf =~ /HTTP\/1.1 101 Switching Protocols/x ) { - - Log3( $name, 4, -"LGTV_WebOS ($name) - received HTTP data string, start response processing: $buf" - ); - LGTV_WebOS_ResponseProcessing( $hash, $buf ); - - } - else { - - Log3( $name, 4, -"LGTV_WebOS ($name) - coruppted data found, run LGTV_WebOS_ProcessRead: $buf" - ); - LGTV_WebOS_ProcessRead( $hash, $buf ); - } - - return; -} - -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} ) && $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 ) - if ( defined($tail) && ($tail) ); - - ( $json, $tail ) = LGTV_WebOS_ParseMsg( $hash, $tail ); - - Log3( $name, 5, - "LGTV_WebOS ($name) - Nach Sub: Laenge JSON: " - . length($json) - . " Content: " - . $json - . " Tail: " - . $tail ); - } - - $tail = '' - if ( length($tail) > 30000 ); - $hash->{PARTIAL} = $tail; - Log3( $name, 4, "LGTV_WebOS ($name) - PARTIAL lenght: " . length($tail) ); - - Log3( $name, 5, "LGTV_WebOS ($name) - Tail: " . $tail ); - Log3( $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL} ); - - return; -} - -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" ); - - return; -} - -sub LGTV_WebOS_ResponseProcessing { - my ( $hash, $response ) = @_; - my $name = $hash->{NAME}; - - ######################## - ### Response has HTML Header - if ( $response =~ /HTTP\/1.1 101 Switching Protocols/x ) { - - 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; - } - - elsif ( $response =~ m/^{"type":".+}}$/x ) { - - return Log3( $name, 4, - "LGTV_WebOS ($name) - garbage after JSON object" ) - if ( $response =~ m/^{"type":".+}}.+{"type":".+/x ); - - Log3( $name, 4, - "LGTV_WebOS ($name) - JSON detected, run LGTV_WebOS_WriteReadings" - ); - - my $json = $response; - - Log3( $name, 4, "LGTV_WebOS ($name) - Corrected JSON String: $json" ) - if ($json); - - if ( !defined($json) || !($json) ) { - - Log3( $name, 4, - "LGTV_WebOS ($name) - Corrected JSON String empty" ); - return; - } - - my $decode_json = eval { decode_json( encode_utf8($json) ) }; - if ($@) { - Log3( $name, 3, - "LGTV_WebOS ($name) - JSON error while request: $@" ); - return; - } - - LGTV_WebOS_WriteReadings( $hash, $decode_json ); - - return; - } - - Log3( $name, 4, "LGTV_WebOS ($name) - no Match found" ); - - return; -} - -sub LGTV_WebOS_WriteServiceReadings { - my $hash = shift; - my $decode_json = shift; - - for my $services ( @{ $decode_json->{payload}{services} } ) { - readingsBulkUpdateIfChanged( - $hash, - 'service_' . $services->{name}, - 'v.' . $services->{version} - ); - } - - return; -} - -sub LGTV_WebOS_WriteDeviceReadings { - my $hash = shift; - my $decode_json = shift; - - for my $devices ( @{ $decode_json->{payload}{devices} } ) { - - if ( !defined( $hash->{helper}{device}{inputs}{ $devices->{label} } ) - || ! - defined( $hash->{helper}{device}{inputapps}{ $devices->{appId} } ) ) - { - - $hash->{helper}{device}{inputs} - { makeDeviceName( $devices->{label} ) } = $devices->{appId}; - $hash->{helper}{device}{inputapps}{ $devices->{appId} } = - makeDeviceName( $devices->{label} ); - } - - readingsBulkUpdateIfChanged( - $hash, - 'extInput_' . makeDeviceName( $devices->{label} ), - 'connect_' . $devices->{connected} - ); - } - - return; -} - -sub LGTV_WebOS_WriteProgramlistReadings { - my $hash = shift; - my $decode_json = shift; - - require Date::Parse; - my $count = 0; - for my $programList ( @{ $decode_json->{payload}{programList} } ) { - - if ( - str2time( - LGTV_WebOS_FormartStartEndTime( $programList->{localEndTime} ) - ) > time() - ) - { - if ( $count < 1 ) { - - readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', - $programList->{programName} ); - readingsBulkUpdateIfChanged( - $hash, - 'channelCurrentStartTime', - LGTV_WebOS_FormartStartEndTime( - $programList->{localStartTime} - ) - ); - readingsBulkUpdateIfChanged( - $hash, - 'channelCurrentEndTime', - LGTV_WebOS_FormartStartEndTime( - $programList->{localEndTime} - ) - ); - - } - elsif ( $count < 2 ) { - - readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', - $programList->{programName} ); - readingsBulkUpdateIfChanged( - $hash, - 'channelNextStartTime', - LGTV_WebOS_FormartStartEndTime( - $programList->{localStartTime} - ) - ); - readingsBulkUpdateIfChanged( - $hash, - 'channelNextEndTime', - LGTV_WebOS_FormartStartEndTime( - $programList->{localEndTime} - ) - ); - } - - $count++; - return if ( $count > 1 ); - } - } - - return; -} - -sub LGTV_WebOS_WriteMuteReadings { - my $hash = shift; - my $decode_json = shift; - - if ( - defined( $decode_json->{payload}{'mute'} ) - && ( $decode_json->{payload}{'mute'} eq 'true' - || $decode_json->{payload}{'mute'} == 1 ) - ) - { - - readingsBulkUpdateIfChanged( $hash, 'mute', 'on' ); - - } - elsif ( defined( $decode_json->{payload}{'mute'} ) ) { - if ( $decode_json->{payload}{'mute'} eq 'false' - || $decode_json->{payload}{'mute'} == 0 ) - { - - readingsBulkUpdateIfChanged( $hash, 'mute', 'off' ); - } - } - - if ( - defined( $decode_json->{payload}{'muted'} ) - && ( $decode_json->{payload}{'muted'} eq 'true' - || $decode_json->{payload}{'muted'} == 1 ) - ) - { - - readingsBulkUpdateIfChanged( $hash, 'mute', 'on' ); - - } - elsif ( - defined( $decode_json->{payload}{'muted'} ) - && ( $decode_json->{payload}{'muted'} eq 'false' - || $decode_json->{payload}{'muted'} == 0 ) - ) - { - - readingsBulkUpdateIfChanged( $hash, 'mute', 'off' ); - } - - return; -} - -sub LGTV_WebOS_Write3dReadings { - my $hash = shift; - my $decode_json = shift; - - if ( $decode_json->{payload}{status3D}{status} eq 'false' - || $decode_json->{payload}{status3D}{status} == 0 ) - { - - readingsBulkUpdateIfChanged( $hash, '3D', 'off' ); - - } - elsif ($decode_json->{payload}{status3D}{status} eq 'true' - || $decode_json->{payload}{status3D}{status} == 1 ) - { - - readingsBulkUpdateIfChanged( $hash, '3D', 'on' ); - } - - readingsBulkUpdateIfChanged( $hash, '3DMode', - $decode_json->{payload}{status3D}{pattern} ); - - return; -} - -sub LGTV_WebOS_WriteAppIdReadings { - my $hash = shift; - my $decode_json = shift; - - if ( - ( - $decode_json->{payload}{appId} =~ /com.webos.app.externalinput/x - || $decode_json->{payload}{appId} =~ /com.webos.app.hdmi/x - ) - && defined( - $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } - ) - && $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } - ) - { - - readingsBulkUpdateIfChanged( $hash, 'input', - $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } - ); - readingsBulkUpdateIfChanged( $hash, 'launchApp', '-' ); - - } - elsif ( defined( $openAppsPackageName{ $decode_json->{payload}{appId} } ) - && $openAppsPackageName{ $decode_json->{payload}{appId} } ) - { - - readingsBulkUpdateIfChanged( $hash, 'launchApp', - $openAppsPackageName{ $decode_json->{payload}{appId} } ); - readingsBulkUpdateIfChanged( $hash, 'input', '-' ); - } - - return; -} - -sub LGTV_WebOS_WriteTypeReadings { - my $hash = shift; - my $decode_json = shift; - - my $response; - - if ( $decode_json->{type} eq 'registered' - && defined( $decode_json->{payload}{'client-key'} ) ) - { - - $hash->{helper}{device}{registered} = 1; - - } - elsif ( - ( - $decode_json->{type} eq 'response' - && ( $decode_json->{payload}{returnValue} eq 'true' - || $decode_json->{payload}{returnValue} == 1 ) - ) - || ( $decode_json->{type} eq 'registered' ) - && defined( $decode_json->{payload}{'client-key'} ) - ) - { - - $response = 'ok'; - readingsBulkUpdateIfChanged( $hash, 'pairing', 'paired' ); - $hash->{helper}{device}{runsetcmd} = - $hash->{helper}{device}{runsetcmd} - 1 - if ( $hash->{helper}{device}{runsetcmd} > 0 ); - - } - elsif ( $decode_json->{type} eq 'error' ) { - - $response = "error - $decode_json->{error}" - if ( $decode_json->{error} ne '404 no such service or method' ); - - if ( $decode_json->{error} eq '401 insufficient permissions' - || $decode_json->{error} eq - '401 insufficient permissions (not registered)' ) - { - - readingsBulkUpdateIfChanged( $hash, 'pairing', 'unpaired' ); - } - - $hash->{helper}{device}{runsetcmd} = - $hash->{helper}{device}{runsetcmd} - 1 - if ( $hash->{helper}{device}{runsetcmd} > 0 ); - } - - return $response; -} - -sub LGTV_WebOS_WriteReadings { - my ( $hash, $decode_json ) = @_; - - my $name = $hash->{NAME}; - my $response; - - Log3( $name, 4, "LGTV_WebOS ($name) - Beginn Readings writing" ); - - readingsBeginUpdate($hash); - - if ( ref( $decode_json->{payload}{services} ) eq "ARRAY" - && scalar( @{ $decode_json->{payload}{services} } ) > 0 ) - { - LGTV_WebOS_WriteServiceReadings( $hash, $decode_json ); - } - elsif ( ref( $decode_json->{payload}{devices} ) eq "ARRAY" - && scalar( @{ $decode_json->{payload}{devices} } ) > 0 ) - { - LGTV_WebOS_WriteDeviceReadings( $hash, $decode_json ); - } - elsif ( ref( $decode_json->{payload}{programList} ) eq "ARRAY" - && scalar( @{ $decode_json->{payload}{programList} } ) > 0 ) - { - LGTV_WebOS_WriteProgramlistReadings( $hash, $decode_json ); - } - - if ( defined( $decode_json->{payload}{'mute'} ) - || defined( $decode_json->{payload}{'muted'} ) ) - { - LGTV_WebOS_WriteMuteReadings( $hash, $decode_json ); - } - elsif ( defined( $decode_json->{payload}{status3D}{status} ) ) { - LGTV_WebOS_Write3dReadings( $hash, $decode_json ); - } - elsif ( defined( $decode_json->{payload}{appId} ) ) { - LGTV_WebOS_WriteAppIdReadings( $hash, $decode_json ); - } - - if ( defined( $decode_json->{type} ) ) { - $response = LGTV_WebOS_WriteTypeReadings( $hash, $decode_json ); - } - - readingsBulkUpdateIfChanged( $hash, 'lgKey', - $decode_json->{payload}{'client-key'} ) - if ( defined( $decode_json->{payload}{'client-key'} ) ); - readingsBulkUpdateIfChanged( $hash, 'volume', - $decode_json->{payload}{'volume'} ) - if ( defined( $decode_json->{payload}{'volume'} ) ); - readingsBulkUpdateIfChanged( $hash, 'lastResponse', $response ) - if ( defined($response) ); - - if ( ReadingsVal( $name, 'launchApp', 'none' ) eq 'TV' ) { - - readingsBulkUpdateIfChanged( $hash, 'channel', - $decode_json->{payload}{'channelNumber'} ) - if ( defined( $decode_json->{payload}{'channelNumber'} ) ); - readingsBulkUpdateIfChanged( $hash, 'channelName', - $decode_json->{payload}{'channelName'} ) - if ( defined( $decode_json->{payload}{'channelName'} ) ); - readingsBulkUpdateIfChanged( $hash, 'channelMedia', - $decode_json->{payload}{'channelTypeName'} ) - if ( defined( $decode_json->{payload}{'channelTypeName'} ) ); - - } - else { - - readingsBulkUpdateIfChanged( $hash, 'channelName', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channel', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelMedia', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime', '-' ); - readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime', '-' ); - } - - readingsBulkUpdateIfChanged( $hash, 'state', 'on' ); - - readingsEndUpdate( $hash, 1 ); - - return; -} - -sub LGTV_WebOS_Pairing { - my $hash = shift; - my $name = $hash->{NAME}; - - 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 Fernbedienung" - }, - "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" ); - - return; -} - -sub LGTV_WebOS_CreateSendCommand { - my ( $hash, $uri, $payload, $type ) = @_; - - my $name = $hash->{NAME}; - - $type = 'request' if ( !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; -} - -sub LGTV_WebOS_Hybi10Encode { - my $payload = shift; - my $type = shift // 'text'; - my $masked = shift // 1; - - my @frameHead; - my $frame = ""; - my $payloadLength = length($payload); - - given ($type) { - when ('text') { - - # first byte indicates FIN, Text-Frame (10000001): - $frameHead[0] = 129; - } - when ('close') { - - # first byte indicates FIN, Close Frame(10001000): - $frameHead[0] = 136; - } - when ('ping') { - - # first byte indicates FIN, Ping frame (10001001): - $frameHead[0] = 137; - } - when ('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; - } - - } - 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}; - - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_GetAudioStatus: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{getAudioStatus}, undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -sub LGTV_WebOS_GetCurrentChannel { - my $hash = shift; - my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash, 'LGTV_WebOS_GetCurrentChannel' ); - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_GetCurrentChannel: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{getCurrentChannel}, undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -sub LGTV_WebOS_GetForgroundAppInfo { - my $hash = shift; - my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash, 'LGTV_WebOS_GetForgroundAppInfo' ); - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_GetForgroundAppInfo: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{getForegroundAppInfo}, - undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -sub LGTV_WebOS_GetExternalInputList { - my $hash = shift; - my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash, 'LGTV_WebOS_GetExternalInputList' ); - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_GetExternalInputList: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{getExternalInputList}, - undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -sub LGTV_WebOS_Get3DStatus { - my $hash = shift; - my $name = $hash->{NAME}; - - RemoveInternalTimer( $hash, 'LGTV_WebOS_Get3DStatus' ); - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_Get3DStatus: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{get3DStatus}, undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -sub LGTV_WebOS_GetChannelProgramInfo { - my $hash = shift; - my $name = $hash->{NAME}; - - Log3( $name, 4, - "LGTV_WebOS ($name) - LGTV_WebOS_GetChannelProgramInfo: " - . $hash->{helper}{device}{runsetcmd} ); - LGTV_WebOS_CreateSendCommand( $hash, $lgCommands{getChannelProgramInfo}, - undef ) - if ( $hash->{helper}{device}{runsetcmd} == 0 ); - - return; -} - -############################################# -### my little Helper - -sub LGTV_WebOS_ParseMsg { - my $hash = shift; - my $buffer = shift; - - my $name = $hash->{NAME}; - my $jsonopen = 0; - my $jsonclose = 0; - my $msg = ''; - my $tail = ''; - - if ($buffer) { - for my $c ( split //, $buffer ) { - if ( $jsonopen == $jsonclose && $jsonopen > 0 ) { - $tail .= $c; - Log3( $name, 5, -"LGTV_WebOS ($name) - $jsonopen == $jsonclose && $jsonopen > 0" - ); - - } - elsif ( ( $jsonopen == $jsonclose ) && ( $c ne '{' ) ) { - - Log3( $name, 5, - "LGTV_WebOS ($name) - Garbage character before message: " - . $c ); - - } - else { - - if ( $c eq '{' ) { - - $jsonopen++; - - } - elsif ( $c eq '}' ) { - - $jsonclose++; - } - - $msg .= $c; - } - } - - if ( $jsonopen != $jsonclose ) { - - $tail = $msg; - $msg = ''; - } - } - - Log3( $name, 5, "LGTV_WebOS ($name) - return msg: $msg and tail: $tail" ); - return ( $msg, $tail ); -} - -sub LGTV_WebOS_Header2Hash { - my $string = shift; - my %hash = (); - - for my $line ( split( "\r\n", $string ) ) { - my ( $key, $value ) = split( ": ", $line ); - next if ( !$value ); - - $value =~ s/^ //x; - $hash{$key} = $value; - } - - return \%hash; -} - -sub LGTV_WebOS_FormartStartEndTime { - my $string = shift; - - my @timeArray = split( ',', $string ); - - return -"$timeArray[0]-$timeArray[1]-$timeArray[2] $timeArray[3]:$timeArray[4]:$timeArray[5]"; -} - -############ Presence Erkennung Begin ################# -sub LGTV_WebOS_Presence { - my $hash = shift; - my $name = $hash->{NAME}; - - $hash->{helper}{RUNNING_PID} = - BlockingCall( "LGTV_WebOS_PresenceRun", $name . '|' . $hash->{HOST}, - "LGTV_WebOS_PresenceDone", 5, "LGTV_WebOS_PresenceAborted", $hash ) - unless ( exists( $hash->{helper}{RUNNING_PID} ) ); - - return; -} - -sub LGTV_WebOS_PresenceRun { - my $string = shift; - my ( $name, $host ) = split( "\\|", $string ); - - my $tmp; - my $response; - - $tmp = qx(ping -c 3 -w 2 $host 2>&1); ## no critic (Backtick operator used) - - if ( defined($tmp) && $tmp ne "" ) { - - chomp $tmp; - Log3( $name, 4, - "LGTV_WebOS ($name) - ping command returned with output:\n$tmp" ); - $response = $name . '|' . ( - $tmp =~ - /\d+ [Bb]ytes (from|von)/ ## no critic (Regular expression without "/x") - && $tmp !~ /[Uu]nreachable/x - ? 'present' - : 'absent' - ); - - } - else { - - $response = "$name|Could not execute ping command"; - } - - Log3( $name, 4, -"Sub LGTV_WebOS_PresenceRun ($name) - Sub finish, Call LGTV_WebOS_PresenceDone" - ); - - return $response; -} - -sub LGTV_WebOS_PresenceDone { - my $string = shift; - - my ( $name, $response ) = split( "\\|", $string ); - my $hash = $defs{$name}; - - delete( $hash->{helper}{RUNNING_PID} ); - - Log3( $name, 4, -"Sub LGTV_WebOS_PresenceDone ($name) - Helper is disabled. Stop processing" - ) if ( $hash->{helper}{DISABLED} ); - return if ( $hash->{helper}{DISABLED} ); - - readingsSingleUpdate( $hash, 'presence', $response, 1 ); - - LGTV_WebOS_SocketClosePresenceAbsent( $hash, $response ); - - Log3( $name, 4, "Sub LGTV_WebOS_PresenceDone ($name) - presence done" ); - - return; -} - -sub LGTV_WebOS_PresenceAborted { - my $hash = shift; - my $name = $hash->{NAME}; - - delete( $hash->{helper}{RUNNING_PID} ); - readingsSingleUpdate( $hash, 'presence', 'pingPresence timedout', 1 ); - - Log3( $name, 4, -"Sub LGTV_WebOS_PresenceAborted ($name) - The BlockingCall Process terminated unexpectedly. Timedout!" - ); - - return; -} - -sub LGTV_WebOS_SocketClosePresenceAbsent { - - my $hash = shift; - my $presence = shift; - - my $name = $hash->{NAME}; - - LGTV_WebOS_Close($hash) - if ( $presence eq 'absent' && !IsDisabled($name) && $hash->{CD} ) - ; # https://forum.fhem.de/index.php/topic,66671.msg694578.html#msg694578 - # Sobald pingPresence absent meldet und der Socket noch steht soll er geschlossen werden, da sonst FHEM nach 4-6 min für 10 min blockiert - - return; -} - -sub LGTV_WebOS_WakeUp_Udp { - my ( $hash, $mac_addr, $host, $port ) = @_; - my $name = $hash->{NAME}; - - $port = 9 if ( !defined $port || $port !~ /^\d+$/x ); - - my $sock = IO::Socket::INET->new( Proto => 'udp' ) or warn "socket : $!\n"; - if ( !$sock ) { - Log3( $name, 3, - "Sub LGTV_WebOS_WakeUp_Udp ($name) - Can't create WOL socket" ); - return 1; - } - - my $ip_addr = inet_aton($host); - my $sock_addr = sockaddr_in( $port, $ip_addr ); - $mac_addr =~ s/://xg; - my $packet = - pack( 'C6H*', 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, $mac_addr x 16 ); - - setsockopt( $sock, SOL_SOCKET, SO_BROADCAST, 1 ) - or warn "setsockopt : $!\n"; - send( $sock, $packet, 0, $sock_addr ) or warn "send : $!\n"; - close($sock); - - return 1; -} - -####### Presence Erkennung Ende ############ - 1; +__END__ + =pod =item device =item summary Controls LG SmartTVs run with WebOS Operating System @@ -2095,6 +348,7 @@ sub LGTV_WebOS_WakeUp_Udp { + =end html_DE =for :application/json;q=META.json 82_LGTV_WebOS.pm @@ -2114,7 +368,7 @@ sub LGTV_WebOS_WakeUp_Udp { ], "release_status": "stable", "license": "GPL_2", - "version": "v3.4.2", + "version": "v3.6.1", "author": [ "Marko Oldenburg " ], diff --git a/fhem/lib/FHEM/Devices/LGTV/LGTVWebOS.pm b/fhem/lib/FHEM/Devices/LGTV/LGTVWebOS.pm new file mode 100644 index 000000000..79cbbec6b --- /dev/null +++ b/fhem/lib/FHEM/Devices/LGTV/LGTVWebOS.pm @@ -0,0 +1,1833 @@ +############################################################################### +# +# Developed with VSCodium and richterger perl plugin. +# +# (c) 2017-2022 Copyright: Marko Oldenburg (fhemdevelopment at cooltux dot net) +# All rights reserved +# +# Special thanks goes to comitters: +# - Vitolinker / Commandref +# +# +# 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$ +# +############################################################################### +package FHEM::Devices::LGTV::LGTVWebOS; + +use strict; +use warnings; +use experimental qw /switch/; + +## try / catch +use Try::Tiny; + +# use Carp; +use autodie qw /:io/; +## + +use FHEM::Meta; +use GPUtils qw(GP_Import); + +#-- Run before package compilation +BEGIN { + #-- Export to main context with different name + GP_Import( + qw( + modules + init_done + selectlist + defs + ) + ); +} + +my $missingModul = ""; + +eval { require MIME::Base64; 1 } or $missingModul .= 'MIME::Base64 '; +eval { require IO::Socket::INET; 1 } or $missingModul .= 'IO::Socket::INET '; + +## no critic (Conditional "use" statement. Use "require" to conditionally include a module (Modules::ProhibitConditionalUseStatements)) +eval { use Digest::SHA qw /sha1_hex/; 1 } or $missingModul .= 'Digest::SHA '; +eval { use Encode qw /encode_utf8 decode_utf8/; 1 } + or $missingModul .= 'Encode '; +## use critic + +eval { require Blocking; 1 } or $missingModul .= 'Blocking '; + +# try to use JSON::MaybeXS wrapper +# for chance of better performance + open code +eval { + require JSON::MaybeXS; + import JSON::MaybeXS qw( decode_json encode_json ); + 1; +} or do { + + # try to use JSON wrapper + # for chance of better performance + eval { + # JSON preference order + local $ENV{PERL_JSON_BACKEND} = + 'Cpanel::JSON::XS,JSON::XS,JSON::PP,JSON::backportPP' + unless ( defined( $ENV{PERL_JSON_BACKEND} ) ); + + require JSON; + import JSON qw( decode_json encode_json ); + 1; + } or do { + + # In rare cases, Cpanel::JSON::XS may + # be installed but JSON|JSON::MaybeXS not ... + eval { + require Cpanel::JSON::XS; + import Cpanel::JSON::XS qw(decode_json encode_json); + 1; + } or do { + + # In rare cases, JSON::XS may + # be installed but JSON not ... + eval { + require JSON::XS; + import JSON::XS qw(decode_json encode_json); + 1; + } or do { + + # Fallback to built-in JSON which SHOULD + # be available since 5.014 ... + eval { + require JSON::PP; + import JSON::PP qw(decode_json encode_json); + 1; + } or do { + + # Fallback to JSON::backportPP in really rare cases + require JSON::backportPP; + import JSON::backportPP qw(decode_json encode_json); + 1; + }; + }; + }; + }; +}; + +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", "channelNumber" ], + "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', + 'AmazonLovefilm' => 'lovefilm.de', + 'AmazonVideo' => 'amazon', + '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', + 'Plex' => 'cdp-30', + 'SkyOnline' => 'de.sky.skyonline', + 'Smart-IPTV' => 'com.1827622.109556', + 'Spotify' => 'spotify-beehive', + 'DuplexIPTV' => 'com.duplexiptv.app', + 'Disney+' => 'com.disney.disneyplus-prod', + 'Smart-IPTV' => 'siptv', + 'AppleTV' => 'com.apple.appletv', + 'Joyn' => 'joyn', + 'YouTube-Kids' => 'youtube.leanback.kids.v4', + 'DAZN' => 'dazn', + 'SkyQ' => 'com.skygo.app.de.q', + 'WaipuTv' => 'tv.waipu.app.waipu-lg', +); + +my %openAppsPackageName = reverse %openApps; + +sub Define { + my $hash = shift; + my $def = shift; + my $version; + + return $@ unless ( FHEM::Meta::SetInternals($hash) ); + + $version = FHEM::Meta::Get( $hash, 'version' ); + our $VERSION = $version; + + my @a = split( "[ \t][ \t]*", $def ); + + return "too few parameters: define LGTV_WebOS " if ( @a != 3 ); + return + "Cannot define LGTV_WebOS device. Perl modul ${missingModul} is missing." + if ($missingModul); + + my $name = $a[0]; + my $host = $a[2]; + + $hash->{HOST} = $host; + $hash->{VERSION} = version->parse($VERSION)->normal; + $hash->{PARTIAL} = ''; + $hash->{helper}{device}{channelguide}{counter} = 0; + $hash->{helper}{device}{registered} = 0; + $hash->{helper}{device}{runsetcmd} = 0; + + ::Log3( $name, 3, "LGTV_WebOS ($name) - defined with host $host" ); + + ::CommandAttr( undef, + $name . ' devStateIcon on:10px-kreis-gruen:off off:10px-kreis-rot:on' ) + if ( ::AttrVal( $name, 'devStateIcon', 'none' ) eq 'none' ); + ::CommandAttr( undef, $name . ' room LGTV' ) + if ( ::AttrVal( $name, 'room', 'none' ) eq 'none' ); + + ::CommandDeleteReading( undef, $name . ' presence' ) + if ( ::AttrVal( $name, 'pingPresence', 0 ) == 0 ); + + $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} } = $hash; + + ::readingsSingleUpdate( $hash, 'state', 'Initialized', 1 ); + + if ($init_done) { + TimerStatusRequest($hash); + } + else { + ::InternalTimer( ::gettimeofday() + 15, + \&FHEM::Devices::LGTV::LGTVWebOS::TimerStatusRequest, $hash ); + } + + $hash->{helper}->{lastResponse} = + int( ::gettimeofday() ); # Check Socket KeepAlive + + return; +} + +sub Undef { + my $hash = shift; + my $arg = shift; + + my $host = $hash->{HOST}; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer($hash); + delete $modules{LGTV_WebOS}{defptr}{ $hash->{HOST} }; + ::Log3( $name, 3, "LGTV_WebOS ($name) - device $name deleted" ); + + return; +} + +sub Attr { + my ( $cmd, $name, $attrName, $attrVal ) = @_; + my $hash = $defs{$name}; + + my $orig = $attrVal; + + if ( $attrName eq "disable" ) { + if ( $cmd eq "set" && $attrVal eq "1" ) { + ::RemoveInternalTimer($hash); + ::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" ); + TimerStatusRequest($hash); + } + } + + 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; +} + +sub TimerStatusRequest { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer( $hash, \&TimerStatusRequest ); + + ::readingsBeginUpdate($hash); + + if ( !::IsDisabled($name) + && $hash->{CD} + && $hash->{helper}{device}{registered} == 1 ) + { + + ::Log3( $name, 4, "LGTV_WebOS ($name) - run get functions" ); + + Presence($hash) + if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 ); + + if ( $hash->{helper}{device}{channelguide}{counter} > 2 + && ::AttrVal( $name, 'channelGuide', 0 ) == 1 + && ::ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' ) + { + + GetChannelProgramInfo($hash); + $hash->{helper}{device}{channelguide}{counter} = 0; + + } + else { + + GetAudioStatus($hash); + ::InternalTimer( ::gettimeofday() + 2, + \&FHEM::Devices::LGTV::LGTVWebOS::GetCurrentChannel, $hash ) + if ( ::ReadingsVal( $name, 'launchApp', 'TV' ) eq 'TV' ); + ::InternalTimer( ::gettimeofday() + 4, + \&FHEM::Devices::LGTV::LGTVWebOS::GetForgroundAppInfo, $hash ); + ::InternalTimer( ::gettimeofday() + 6, + \&FHEM::Devices::LGTV::LGTVWebOS::Get3DStatus, $hash ); + ::InternalTimer( ::gettimeofday() + 8, + \&FHEM::Devices::LGTV::LGTVWebOS::GetExternalInputList, $hash ); + } + + } + elsif ( ::IsDisabled($name) ) { + + Close($hash); + Presence($hash) + if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 ); + $hash->{helper}{device}{runsetcmd} = 0; + ::readingsBulkUpdateIfChanged( $hash, 'state', 'disabled' ); + + } + else { + ::readingsSingleUpdate( $hash, 'state', 'off', 1 ) + if ( ::ReadingsVal( $name, 'state', 'off' ) ne 'off' ); + + Presence($hash) + if ( ::AttrVal( $name, 'pingPresence', 0 ) == 1 ); + + ::readingsBulkUpdateIfChanged( $hash, 'channel', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelName', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelMedia', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime', '-' ); + + $hash->{helper}{device}{runsetcmd} = 0; + } + + ::readingsEndUpdate( $hash, 1 ); + + Open($hash) if ( !::IsDisabled($name) && !$hash->{CD} ); + + $hash->{helper}{device}{channelguide}{counter} = + $hash->{helper}{device}{channelguide}{counter} + 1; + ::InternalTimer( ::gettimeofday() + 10, + \&FHEM::Devices::LGTV::LGTVWebOS::TimerStatusRequest, $hash ); + + SocketKeepAlive($hash) + if ( ::AttrVal( $name, 'keepAliveCheckTime', 0 ) > 0 ) + ; # Check Socket KeepAlive + + return; +} + +sub Set { ## no critic (Subroutine "Set" with high complexity score) + my ( $hash, $name, $cmd, @args ) = @_; + my ( $arg, @params ) = @args; + + my $uri; + my %payload; + + given ($cmd) { + when ('connect') { + return "usage: connect" if ( @args != 0 ); + + Open($hash); + + return; + + } + when ('clearInputList') { + return "usage: clearInputList" if ( @args != 0 ); + + delete $hash->{helper}{device}{inputs}; + delete $hash->{helper}{device}{inputapps}; + + return; + + } + when ('pairing') { + return "usage: pairing" if ( @args != 0 ); + + Pairing($hash); + + return; + + } + when ('screenMsg') { + return "usage: screenMsg " if ( @args < 1 ); + + my $msg = join( " ", @args ); + $payload{ $lgCommands{$cmd}->[1] } = decode_utf8($msg); + $uri = $lgCommands{$cmd}->[0]; + + } + when ('off') { + return "usage: on/off" if ( @args != 0 ); + + $uri = $lgCommands{powerOff}; + } + when ('on') { + if ( ::AttrVal( $name, 'wakeOnLanMAC', 'none' ) ne 'none' ) { + WakeUp_Udp( + $hash, + ::AttrVal( $name, 'wakeOnLanMAC', 0 ), + ::AttrVal( $name, 'wakeOnLanBroadcast', '255.255.255.255' ) + ); + return; + } + elsif ( ::AttrVal( $name, 'wakeupCmd', 'none' ) ne 'none' ) { + my $wakeupCmd = ::AttrVal( $name, 'wakeupCmd', 'none' ); + if ( $wakeupCmd =~ s/^[ \t]*\{|\}[ \t]*$//xg ) { + ::Log3( $name, 4, +"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd" + ); + eval { $wakeupCmd } or do { + ::Log3( $name, 2, +"LGTV_WebOS executing wake-up command (Perl): $wakeupCmd failed" + ); + return; + }; + return; + } + else { + ::Log3( $name, 4, +"LGTV_WebOS executing wake-up command (fhem): $wakeupCmd" + ); + ::fhem $wakeupCmd; + return; + } + } + else { + $uri = $lgCommands{powerOn}; + } + } + when ('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'}; + } + + } + when ('mute') { + return "usage: mute" if ( @args != 1 ); + + if ( $args[0] eq 'off' ) { + + $uri = $lgCommands{volumeDown}->[0]; + + } + elsif ( $args[0] eq 'on' ) { + + $payload{ $lgCommands{$cmd}->[1] } = 'true'; + $uri = $lgCommands{$cmd}->[0]; + } + + } + when ('volume') { + return "usage: volume" if ( @args != 1 ); + + $payload{ $lgCommands{$cmd}->[1] } = int( join( " ", @args ) ); + $uri = $lgCommands{$cmd}->[0]; + + } + when ('launchApp') { + return "usage: launchApp" if ( @args != 1 ); + + $payload{ $lgCommands{$cmd}->[1] } = + $openApps{ join( " ", @args ) }; + $uri = $lgCommands{$cmd}->[0]; + + } + when ('input') { + return "usage: input" if ( @args != 1 ); + + my $inputLabel = join( " ", @args ); + $payload{ $lgCommands{launchApp}->[1] } = + $hash->{helper}{device}{inputs}{$inputLabel}; + $uri = $lgCommands{launchApp}->[0]; + + } + when ('volumeUp') { + return "usage: volumeUp" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('volumeDown') { + return "usage: volumeDown" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('channelDown') { + return "usage: channelDown" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('channelUp') { + return "usage: channelUp" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('channel') { + return "usage: channel" if ( @args != 1 ); + + $payload{ $lgCommands{openChannel}->[1] } = join( " ", @args ); + $uri = $lgCommands{openChannel}->[0]; + + } + when ('getServiceList') { + return "usage: getServiceList" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('getChannelList') { + return "usage: getChannelList" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('getAppList') { + return "usage: getAppList" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('getExternalInputList') { + return "usage: getExternalInputList" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('play') { + return "usage: play" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('stop') { + return "usage: stop" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('fastForward') { + return "usage: fastForward" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('rewind') { + return "usage: rewind" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + when ('pause') { + return "usage: pause" if ( @args != 0 ); + + $uri = $lgCommands{$cmd}->[0]; + + } + default { + my $list = ""; + $list .= +'connect:noArg pairing:noArg screenMsg mute:on,off volume:slider,0,1,100 volumeUp:noArg volumeDown:noArg channelDown:noArg channelUp:noArg getServiceList:noArg on:noArg off:noArg'; + $list .= +' 3D:on,off stop:noArg play:noArg pause:noArg rewind:noArg fastForward:noArg clearInputList:noArg channel'; + $list .= + ## no critic (Expression form of map. See page 169 of PBP) + ' launchApp:' . join( ',', => map qq{$_} => keys %openApps ); + $list .= ' input:' . join( + ',', + ## no critic (Expression form of map. See page 169 of PBP) + => map qq{$_} => keys %{ $hash->{helper}{device}{inputs} } + ) + if ( exists( $hash->{helper}{device}{inputs} ) + && ref( $hash->{helper}{device}{inputs} ) eq "HASH" ); + + return "Unknown argument $cmd, choose one of $list"; + } + } + + $hash->{helper}{device}{runsetcmd} = $hash->{helper}{device}{runsetcmd} + 1; + return CreateSendCommand( $hash, $uri, \%payload ); +} + +sub 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 = IO::Socket::INET->new( + PeerHost => $host, + PeerPort => $port, + Proto => 'tcp', + KeepAlive => 1, + 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; + + $hash->{helper}->{lastResponse} = + int( ::gettimeofday() ); # Check Socket KeepAlive + + ::Log3( $name, 4, "LGTV_WebOS ($name) - Socket Connected" ); + + Handshake($hash); + ::Log3( $name, 4, "LGTV_WebOS ($name) - start Handshake" ); + + return; +} + +sub Close { + my $hash = shift; + my $name = $hash->{NAME}; + + return if ( !$hash->{CD} ); + + delete( $hash->{PARTIAL} ); + + close( $hash->{CD} ) if ( $hash->{CD} ); + delete( $hash->{CD} ); + + delete( $selectlist{$name} ); + delete( $hash->{FD} ); + + ::readingsSingleUpdate( $hash, 'state', 'off', 1 ); + + ::Log3( $name, 4, "LGTV_WebOS ($name) - Socket Disconnected" ); + + return; +} + +sub Write { + my $hash = shift; + my $string = shift; + + 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; +} + +sub SocketKeepAlive { + my $hash = shift; + my $name = $hash->{NAME}; + + if ( + int( ::gettimeofday() ) - int( $hash->{helper}->{lastResponse} ) > + ::AttrVal( $name, 'keepAliveCheckTime', 0 ) ) + { + SocketClosePresenceAbsent( $hash, 'absent' ); + ::Log3( $name, 4, +"LGTV_WebOS ($name) - KeepAlive It looks like there no Data more response" + ); + } + + return; +} + +sub Read { + my $hash = shift; + my $name = $hash->{NAME}; + + my $len; + my $buf; + + ::Log3( $name, 4, "LGTV_WebOS ($name) - ReadFn started" ); + + $hash->{helper}->{lastResponse} = + int( ::gettimeofday() ); # Check Socket KeepAlive + $len = sysread( $hash->{CD}, $buf, 10240 ); + + if ( !defined($len) || !$len ) { + + Close($hash); + + return; + } + + unless ( defined $buf ) { + ::Log3( $name, 3, "LGTV_WebOS ($name) - no data received" ); + return; + } + + if ( $buf =~ /(\{"type":".+}}$)/x ) { + + $buf =~ /(\{"type":".+}}$)/x; + ## no critic (Capture variable used outside conditional. See page 253 of PBP) + $buf = $1; + ## use critic + + ::Log3( $name, 4, +"LGTV_WebOS ($name) - received correct JSON string, start response processing: $buf" + ); + + ResponseProcessing( $hash, $buf ); + + } + elsif ( $buf =~ /HTTP\/1.1 101 Switching Protocols/x ) { + + ::Log3( $name, 4, +"LGTV_WebOS ($name) - received HTTP data string, start response processing: $buf" + ); + + ResponseProcessing( $hash, $buf ); + + } + else { + + ::Log3( $name, 4, +"LGTV_WebOS ($name) - coruppted data found, run LGTV_WebOS_ProcessRead: $buf" + ); + + ProcessRead( $hash, $buf ); + } + + return; +} + +sub ProcessRead { + my $hash = shift; + my $data = shift; + + my $name = $hash->{NAME}; + + my $buffer = ''; + + ::Log3( $name, 4, "LGTV_WebOS ($name) - process read" ); + + if ( exists( $hash->{PARTIAL} ) && $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 ) = 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 ); + + ResponseProcessing( $hash, $json ) + if ( defined($tail) && ($tail) ); + + ( $json, $tail ) = ParseMsg( $hash, $tail ); + + ::Log3( $name, 5, + "LGTV_WebOS ($name) - Nach Sub: Laenge JSON: " + . length($json) + . " Content: " + . $json + . " Tail: " + . $tail ); + } + + $tail = '' + if ( length($tail) > 30000 ); + $hash->{PARTIAL} = $tail; + ::Log3( $name, 4, "LGTV_WebOS ($name) - PARTIAL lenght: " . length($tail) ); + + ::Log3( $name, 5, "LGTV_WebOS ($name) - Tail: " . $tail ); + ::Log3( $name, 5, "LGTV_WebOS ($name) - PARTIAL: " . $hash->{PARTIAL} ); + + return; +} + +sub 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"; + + Write( $hash, $wsHandshakeCmd ); + + $hash->{helper}{wsKey} = $wsKey; + + ::Log3( $name, 4, "LGTV_WebOS ($name) - send Handshake to WriteFn" ); + + TimerStatusRequest($hash); + ::Log3( $name, 4, "LGTV_WebOS ($name) - start timer status request" ); + + Pairing($hash); + ::Log3( $name, 4, "LGTV_WebOS ($name) - start pairing routine" ); + + return; +} + +sub ResponseProcessing { + my ( $hash, $response ) = @_; + my $name = $hash->{NAME}; + + ######################## + ### Response has HTML Header + if ( $response =~ /HTTP\/1.1 101 Switching Protocols/x ) { + + my $data = $response; + my $header = Header2Hash($data); + + ################################ + ### Handshake for first Connect + if ( exists( $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 { + Close($hash); + + ::Log3( $name, 3, +"LGTV_WebOS ($name) - ERROR: Unsucessfull WS connection to $hash->{HOST}" + ); + } + } + + return; + } + + elsif ( $response =~ m/^{"type":".+}}$/x ) { + + return ::Log3( $name, 4, + "LGTV_WebOS ($name) - garbage after JSON object" ) + if ( $response =~ m/^{"type":".+}}.+{"type":".+/x ); + + ::Log3( $name, 4, + "LGTV_WebOS ($name) - JSON detected, run LGTV_WebOS_WriteReadings" + ); + + my $json = $response; + + ::Log3( $name, 4, "LGTV_WebOS ($name) - Corrected JSON String: $json" ) + if ($json); + + if ( !defined($json) || !($json) ) { + + ::Log3( $name, 4, + "LGTV_WebOS ($name) - Corrected JSON String empty" ); + return; + } + + my $decode_json; + try { + $decode_json = decode_json( encode_utf8($json) ); + } + catch { + if ( $_->isa('autodie::exception') && $_->matches(':io') ) { + Log3( $name, 3, + "LGTV_WebOS ($name) autodie - JSON error while request: $_" + ); + return; + } + else { + Log3( $name, 3, + "LGTV_WebOS ($name) - JSON error while request: $_" ); + return; + } + }; # Note semicolon. + + WriteReadings( $hash, $decode_json ); + + return; + } + + ::Log3( $name, 4, "LGTV_WebOS ($name) - no Match found" ); + + return; +} + +sub WriteServiceReadings { + my $hash = shift; + my $decode_json = shift; + + for my $services ( @{ $decode_json->{payload}{services} } ) { + ::readingsBulkUpdateIfChanged( + $hash, + 'service_' . $services->{name}, + 'v.' . $services->{version} + ); + } + + return; +} + +sub WriteDeviceReadings { + my $hash = shift; + my $decode_json = shift; + + for my $devices ( @{ $decode_json->{payload}{devices} } ) { + + if ( !exists( $hash->{helper}{device}{inputs}{ $devices->{label} } ) + || ! + exists( $hash->{helper}{device}{inputapps}{ $devices->{appId} } ) ) + { + + $hash->{helper}{device}{inputs} + { ::makeDeviceName( $devices->{label} ) } = $devices->{appId}; + $hash->{helper}{device}{inputapps}{ $devices->{appId} } = + ::makeDeviceName( $devices->{label} ); + } + + ::readingsBulkUpdateIfChanged( + $hash, + 'extInput_' . ::makeDeviceName( $devices->{label} ), + 'connect_' . $devices->{connected} + ); + } + + return; +} + +sub WriteProgramlistReadings { + my $hash = shift; + my $decode_json = shift; + + require Date::Parse; + my $count = 0; + for my $programList ( @{ $decode_json->{payload}{programList} } ) { + + if ( + ::str2time( FormartStartEndTime( $programList->{localEndTime} ) ) > + time() ) + { + if ( $count < 1 ) { + + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', + $programList->{programName} ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', + FormartStartEndTime( $programList->{localStartTime} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime', + FormartStartEndTime( $programList->{localEndTime} ) ); + + } + elsif ( $count < 2 ) { + + ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', + $programList->{programName} ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime', + FormartStartEndTime( $programList->{localStartTime} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime', + FormartStartEndTime( $programList->{localEndTime} ) ); + } + + $count++; + return if ( $count > 1 ); + } + } + + return; +} + +sub WriteMuteReadings { + my $hash = shift; + my $decode_json = shift; + + if ( + exists( $decode_json->{payload}{'mute'} ) + && ( $decode_json->{payload}{'mute'} eq 'true' + || $decode_json->{payload}{'mute'} == 1 ) + ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'mute', 'on' ); + + } + elsif ( exists( $decode_json->{payload}{'mute'} ) ) { + if ( $decode_json->{payload}{'mute'} eq 'false' + || $decode_json->{payload}{'mute'} == 0 ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'mute', 'off' ); + } + } + + if ( + exists( $decode_json->{payload}{'muted'} ) + && ( $decode_json->{payload}{'muted'} eq 'true' + || $decode_json->{payload}{'muted'} == 1 ) + ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'mute', 'on' ); + + } + elsif ( + exists( $decode_json->{payload}{'muted'} ) + && ( $decode_json->{payload}{'muted'} eq 'false' + || $decode_json->{payload}{'muted'} == 0 ) + ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'mute', 'off' ); + } + + return; +} + +sub Write3dReadings { + my $hash = shift; + my $decode_json = shift; + + if ( $decode_json->{payload}{status3D}{status} eq 'false' + || $decode_json->{payload}{status3D}{status} == 0 ) + { + + ::readingsBulkUpdateIfChanged( $hash, '3D', 'off' ); + + } + elsif ($decode_json->{payload}{status3D}{status} eq 'true' + || $decode_json->{payload}{status3D}{status} == 1 ) + { + + ::readingsBulkUpdateIfChanged( $hash, '3D', 'on' ); + } + + ::readingsBulkUpdateIfChanged( $hash, '3DMode', + $decode_json->{payload}{status3D}{pattern} ); + + return; +} + +sub WriteAppIdReadings { + my $hash = shift; + my $decode_json = shift; + + if ( + ( + $decode_json->{payload}{appId} =~ /com.webos.app.externalinput/x + || $decode_json->{payload}{appId} =~ /com.webos.app.hdmi/x + ) + && exists( + $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } + ) + && $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } + ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'input', + $hash->{helper}{device}{inputapps}{ $decode_json->{payload}{appId} } + ); + ::readingsBulkUpdateIfChanged( $hash, 'launchApp', '-' ); + + } + elsif ( exists( $openAppsPackageName{ $decode_json->{payload}{appId} } ) + && $openAppsPackageName{ $decode_json->{payload}{appId} } ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'launchApp', + $openAppsPackageName{ $decode_json->{payload}{appId} } ); + ::readingsBulkUpdateIfChanged( $hash, 'input', '-' ); + } + + return; +} + +sub WriteTypeReadings { + my $hash = shift; + my $decode_json = shift; + + my $response; + + if ( $decode_json->{type} eq 'registered' + && exists( $decode_json->{payload}{'client-key'} ) ) + { + + $hash->{helper}{device}{registered} = 1; + + } + elsif ( + ( + $decode_json->{type} eq 'response' + && ( $decode_json->{payload}{returnValue} eq 'true' + || $decode_json->{payload}{returnValue} == 1 ) + ) + || ( $decode_json->{type} eq 'registered' ) + && exists( $decode_json->{payload}{'client-key'} ) + ) + { + + $response = 'ok'; + ::readingsBulkUpdateIfChanged( $hash, 'pairing', 'paired' ); + $hash->{helper}{device}{runsetcmd} = + $hash->{helper}{device}{runsetcmd} - 1 + if ( $hash->{helper}{device}{runsetcmd} > 0 ); + + } + elsif ( $decode_json->{type} eq 'error' ) { + + $response = "error - $decode_json->{error}" + if ( $decode_json->{error} ne '404 no such service or method' ); + + if ( $decode_json->{error} eq '401 insufficient permissions' + || $decode_json->{error} eq + '401 insufficient permissions (not registered)' ) + { + + ::readingsBulkUpdateIfChanged( $hash, 'pairing', 'unpaired' ); + } + + $hash->{helper}{device}{runsetcmd} = + $hash->{helper}{device}{runsetcmd} - 1 + if ( $hash->{helper}{device}{runsetcmd} > 0 ); + } + + return $response; +} + +sub WriteReadings { + my ( $hash, $decode_json ) = @_; + + my $name = $hash->{NAME}; + my $response; + + ::Log3( $name, 4, "LGTV_WebOS ($name) - Beginn Readings writing" ); + + ::readingsBeginUpdate($hash); + + if ( ref( $decode_json->{payload}{services} ) eq "ARRAY" + && scalar( @{ $decode_json->{payload}{services} } ) > 0 ) + { + WriteServiceReadings( $hash, $decode_json ); + } + elsif ( ref( $decode_json->{payload}{devices} ) eq "ARRAY" + && scalar( @{ $decode_json->{payload}{devices} } ) > 0 ) + { + WriteDeviceReadings( $hash, $decode_json ); + } + elsif ( ref( $decode_json->{payload}{programList} ) eq "ARRAY" + && scalar( @{ $decode_json->{payload}{programList} } ) > 0 ) + { + WriteProgramlistReadings( $hash, $decode_json ); + } + + if ( exists( $decode_json->{payload}{'mute'} ) + || exists( $decode_json->{payload}{'muted'} ) ) + { + WriteMuteReadings( $hash, $decode_json ); + } + elsif ( exists( $decode_json->{payload}{status3D}{status} ) ) { + Write3dReadings( $hash, $decode_json ); + } + elsif ( exists( $decode_json->{payload}{appId} ) ) { + WriteAppIdReadings( $hash, $decode_json ); + } + + if ( exists( $decode_json->{type} ) ) { + $response = WriteTypeReadings( $hash, $decode_json ); + } + + ::readingsBulkUpdateIfChanged( $hash, 'lgKey', + $decode_json->{payload}{'client-key'} ) + if ( exists( $decode_json->{payload}{'client-key'} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'volume', + $decode_json->{payload}{'volume'} ) + if ( exists( $decode_json->{payload}{'volume'} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'lastResponse', $response ) + if ( defined($response) ); + + if ( ::ReadingsVal( $name, 'launchApp', 'none' ) eq 'TV' ) { + + ::readingsBulkUpdateIfChanged( $hash, 'channel', + $decode_json->{payload}{'channelNumber'} ) + if ( exists( $decode_json->{payload}{'channelNumber'} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'channelName', + $decode_json->{payload}{'channelName'} ) + if ( exists( $decode_json->{payload}{'channelName'} ) ); + ::readingsBulkUpdateIfChanged( $hash, 'channelMedia', + $decode_json->{payload}{'channelTypeName'} ) + if ( exists( $decode_json->{payload}{'channelTypeName'} ) ); + + } + else { + + ::readingsBulkUpdateIfChanged( $hash, 'channelName', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channel', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelMedia', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentTitle', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentStartTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelCurrentEndTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextTitle', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextStartTime', '-' ); + ::readingsBulkUpdateIfChanged( $hash, 'channelNextEndTime', '-' ); + } + + ::readingsBulkUpdateIfChanged( $hash, 'state', 'on' ); + + ::readingsEndUpdate( $hash, 1 ); + + return; +} + +sub Pairing { + my $hash = shift; + my $name = $hash->{NAME}; + + ::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 Fernbedienung" + }, + "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) ); + + CreateSendCommand( $hash, undef, $usedHandshake, 'register' ); + ::Log3( $name, 4, "LGTV_WebOS ($name) - Send pairing informations" ); + + return; +} + +sub CreateSendCommand { + my ( $hash, $uri, $payload, $type ) = @_; + + my $name = $hash->{NAME}; + + $type = 'request' if ( !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; + try { + $cmd = encode_json($command); + } + catch { + if ( $_->isa('autodie::exception') && $_->matches(':io') ) { + Log3( $name, 3, + "LGTV_WebOS ($name) - can't $cmd encode to json: $_" ); + return; + } + else { + Log3( $name, 3, + "LGTV_WebOS ($name) - can't $cmd encode to json: $_" ); + return; + } + }; + + ::Log3( $name, 5, "LGTV_WebOS ($name) - Sending command: $cmd" ); + + Write( $hash, Hybi10Encode( $cmd, "text", 1 ) ); + + return; +} + +sub Hybi10Encode { + my $payload = shift; + my $type = shift // 'text'; + my $masked = shift // 1; + + my @frameHead; + my $frame = ""; + my $payloadLength = length($payload); + + given ($type) { + when ('text') { + + # first byte indicates FIN, Text-Frame (10000001): + $frameHead[0] = 129; + } + when ('close') { + + # first byte indicates FIN, Close Frame(10001000): + $frameHead[0] = 136; + } + when ('ping') { + + # first byte indicates FIN, Ping frame (10001001): + $frameHead[0] = 137; + } + when ('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; + } + + } + 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 GetAudioStatus { + my $hash = shift; + my $name = $hash->{NAME}; + + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_GetAudioStatus: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{getAudioStatus}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +sub GetCurrentChannel { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer( $hash, + \&FHEM::Devices::LGTV::LGTVWebOS::GetCurrentChannel ); + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_GetCurrentChannel: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{getCurrentChannel}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +sub GetForgroundAppInfo { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer( $hash, + \&FHEM::Devices::LGTV::LGTVWebOS::GetForgroundAppInfo ); + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_GetForgroundAppInfo: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{getForegroundAppInfo}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +sub GetExternalInputList { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer( $hash, + \&FHEM::Devices::LGTV::LGTVWebOS::GetExternalInputList ); + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_GetExternalInputList: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{getExternalInputList}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +sub Get3DStatus { + my $hash = shift; + my $name = $hash->{NAME}; + + ::RemoveInternalTimer( $hash, + \&FHEM::Devices::LGTV::LGTVWebOS::Get3DStatus ); + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_Get3DStatus: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{get3DStatus}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +sub GetChannelProgramInfo { + my $hash = shift; + my $name = $hash->{NAME}; + + ::Log3( $name, 4, + "LGTV_WebOS ($name) - LGTV_WebOS_GetChannelProgramInfo: " + . $hash->{helper}{device}{runsetcmd} ); + CreateSendCommand( $hash, $lgCommands{getChannelProgramInfo}, undef ) + if ( $hash->{helper}{device}{runsetcmd} == 0 ); + + return; +} + +############################################# +### my little Helper + +sub ParseMsg { + my $hash = shift; + my $buffer = shift; + + my $name = $hash->{NAME}; + my $jsonopen = 0; + my $jsonclose = 0; + my $msg = ''; + my $tail = ''; + + if ($buffer) { + for my $c ( split //, $buffer ) { + if ( $jsonopen == $jsonclose && $jsonopen > 0 ) { + $tail .= $c; + ::Log3( $name, 5, +"LGTV_WebOS ($name) - $jsonopen == $jsonclose && $jsonopen > 0" + ); + + } + elsif ( ( $jsonopen == $jsonclose ) && ( $c ne '{' ) ) { + + ::Log3( $name, 5, + "LGTV_WebOS ($name) - Garbage character before message: " + . $c ); + + } + else { + + if ( $c eq '{' ) { + + $jsonopen++; + + } + elsif ( $c eq '}' ) { + + $jsonclose++; + } + + $msg .= $c; + } + } + + if ( $jsonopen != $jsonclose ) { + + $tail = $msg; + $msg = ''; + } + } + + ::Log3( $name, 5, "LGTV_WebOS ($name) - return msg: $msg and tail: $tail" ); + return ( $msg, $tail ); +} + +sub Header2Hash { + my $string = shift; + my %hash = (); + + for my $line ( split( "\r\n", $string ) ) { + my ( $key, $value ) = split( ": ", $line ); + next if ( !$value ); + + $value =~ s/^ //x; + $hash{$key} = $value; + } + + return \%hash; +} + +sub FormartStartEndTime { + my $string = shift; + + my @timeArray = split( ',', $string ); + + return +"$timeArray[0]-$timeArray[1]-$timeArray[2] $timeArray[3]:$timeArray[4]:$timeArray[5]"; +} + +############ Presence Erkennung Begin ################# +sub Presence { + my $hash = shift; + my $name = $hash->{NAME}; + + $hash->{helper}{RUNNING_PID} = ::BlockingCall( + 'FHEM::Devices::LGTV::LGTVWebOS::PresenceRun', + $name . '|' . $hash->{HOST}, + 'FHEM::Devices::LGTV::LGTVWebOS::PresenceDone', + 5, + 'FHEM::Devices::LGTV::LGTVWebOS::PresenceAborted', + $hash + ) unless ( exists( $hash->{helper}{RUNNING_PID} ) ); + + return; +} + +sub PresenceRun { + my $string = shift; + my ( $name, $host ) = split( "\\|", $string ); + + my $tmp; + my $response; + + $tmp = qx(ping -c 3 -w 2 $host 2>&1); ## no critic (Backtick operator used) + + if ( defined($tmp) && $tmp ne '' ) { + + chomp $tmp; + ::Log3( $name, 4, + "LGTV_WebOS ($name) - ping command returned with output:\n$tmp" ); + $response = $name . '|' . ( + $tmp =~ + /\d+ [Bb]ytes (from|von)/ ## no critic (Regular expression without "/x") + && $tmp !~ /[Uu]nreachable/x + ? 'present' + : 'absent' + ); + + } + else { + + $response = "$name|Could not execute ping command"; + } + + ::Log3( $name, 4, + "sub PresenceRun ($name) - Sub finish, Call LGTV_WebOS_PresenceDone" ); + + return $response; +} + +sub PresenceDone { + my $string = shift; + + my ( $name, $response ) = split( "\\|", $string ); + my $hash = $defs{$name}; + + delete( $hash->{helper}{RUNNING_PID} ); + + if ( exists( $hash->{helper}{DISABLED} ) ) { + ::Log3( $name, 4, + "sub PresenceDone ($name) - Helper is disabled. Stop processing" ); + + return; + } + + ::readingsSingleUpdate( $hash, 'presence', $response, 1 ); + + SocketClosePresenceAbsent( $hash, $response ); + + ::Log3( $name, 4, "sub PresenceDone ($name) - presence done" ); + + return; +} + +sub PresenceAborted { + my $hash = shift; + my $name = $hash->{NAME}; + + delete( $hash->{helper}{RUNNING_PID} ); + ::readingsSingleUpdate( $hash, 'presence', 'pingPresence timedout', 1 ); + + ::Log3( $name, 4, +"sub PresenceAborted ($name) - The BlockingCall Process terminated unexpectedly. Timedout!" + ); + + return; +} + +sub SocketClosePresenceAbsent { + + my $hash = shift; + my $presence = shift; + + my $name = $hash->{NAME}; + + Close($hash) + if ( $presence eq 'absent' && !::IsDisabled($name) && $hash->{CD} ) + ; # https://forum.fhem.de/index.php/topic,66671.msg694578.html#msg694578 + # Sobald pingPresence absent meldet und der Socket noch steht soll er geschlossen werden, da sonst FHEM nach 4-6 min für 10 min blockiert + + return; +} + +sub WakeUp_Udp { + my ( $hash, $mac_addr, $host, $port ) = @_; + my $name = $hash->{NAME}; + + $port = 9 if ( !defined $port || $port !~ /^\d+$/x ); + + my $sock = IO::Socket::INET->new( Proto => 'udp' ) or warn "socket : $!\n"; + if ( !$sock ) { + ::Log3( $name, 3, "sub WakeUp_Udp ($name) - Can't create WOL socket" ); + return 1; + } + + my $ip_addr = ::inet_aton($host); + my $sock_addr = ::sockaddr_in( $port, $ip_addr ); + $mac_addr =~ s/://xg; + my $packet = + pack( 'C6H*', 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, $mac_addr x 16 ); + + setsockopt( $sock, ::SOL_SOCKET, ::SO_BROADCAST, 1 ) + or warn "setsockopt : $!\n"; + send( $sock, $packet, 0, $sock_addr ) or warn "send : $!\n"; + close($sock); + + return 1; +} + +####### Presence Erkennung Ende ############ + +1; + +__END__