diff --git a/fhem/FHEM/70_SamsungAV.pm b/fhem/FHEM/70_SamsungAV.pm new file mode 100644 index 000000000..60ba1131a --- /dev/null +++ b/fhem/FHEM/70_SamsungAV.pm @@ -0,0 +1,1432 @@ +############################################################################## +# +# 70_SamsungAV.pm +# +# module to send messages or commands to a Samsung TV +# +# written 2019 by kölnsolar +# based on 70_STV which supports older generation of TV's. Thanks to Zwiebel. +# extended for newer moduls. Python script samsungctl was used as reference. +# +# $Id$ +# +# Version = 1.0 +# +############################################################################## + +package main; +use strict; +use warnings; +use IO::Socket::INET; +use Sys::Hostname; +use MIME::Base64; +use DevIo; +use HttpUtils; + +my @gets = ('dummy'); + +sub SamsungAV_Initialize($) +{ + my ($hash) = @_; + $hash->{DefFn} = "SamsungAV_Define"; + $hash->{UndefFn} = "SamsungAV_Undefine"; + $hash->{StateFn} = "SamsungAV_SetState"; + $hash->{SetFn} = "SamsungAV_Set"; + $hash->{AttrFn} = "SamsungAV_Attr"; + $hash->{NotifyFn} = "SamsungAV_Notify"; + $hash->{ReadFn} = "SamsungAV_Read"; + $hash->{ReadyFn} = "SamsungAV_Ready"; + $hash->{AttrList} = "callerURI screenURI delayRC delayMacro disable:0,1 " . $readingFnAttributes;; + + $data{RC_layout}{SamsungAV_TV} = "SamsungAV_RClayout_TV"; + $data{RC_layout}{SamsungAV_TV_SVG} = "SamsungAV_RClayout_TV_SVG"; + $data{RC_makenotify}{SamsungAV} = "SamsungAV_RCmakenotify"; +} + +sub SamsungAV_Define($$) +{ + my ($hash, $def) = @_; +# DevIo_CloseDev($hash); + my @args = split("[ \t]+", $def); + + if (int(@args) < 4) + { + return "[SamsungAV] Define: not enough arguments. Usage:\n" . + "define SamsungAV "; + } + + $hash->{Host} = $args[2]; + $hash->{Port} = $args[3]; + + if ( $hash->{Port} ne "55000" && $hash->{Port} ne "52235" && $hash->{Port} ne "8000" && $hash->{Port} ne "8001" ){ + return "[SamsungAV] Port is not supported"; + } + if (defined $args[4]) { + $hash->{Mode} = $args[4]; + if (!defined ReadingsVal($args[4],"state",undef)) { return "[SamsungAV] Define: optional DLNA device not known. $hash->{Mode} instead" } + $hash->{".validcommands"} .= "volume:slider,0,1,100 sayText "; + } + else { $hash->{Mode} = undef } + + if ( $hash->{Port} eq 55000 || $hash->{Port} eq "8000" || $hash->{Port} eq "8001"){ + $hash->{".validcommands"} .= "0:noArg 1:noArg 2:noArg 3:noArg 4:noArg 5:noArg 6:noArg 7:noArg 8:noArg 9:noArg ". +"ad:noArg aspect:noArg av1:noArg av2:noArg channel:selectnumbers,0,1,99,0,lin channelDown:noArg channelUp:noArg channelList:noArg clear:noArg component1:noArg component2:noArg ". +"contents:noArg convergence:noArg cyan:noArg down:noArg enter:noArg esaving:noArg exit:noArg ff:noArg guide:noArg green:noArg hdmi:noArg hdmi1:noArg ". +"hdmi2:noArg help:noArg home:noArg info:noArg left:noArg menu:noArg mute:noArg pause:noArg pip_chdown:noArg pip_chup:noArg pip_onoff:noArg play:noArg ". +"pmode:noArg power:noArg poweroff:noArg poweron:noArg prech:noArg program:noArg red:noArg return:noArg rec:noArg rewind:noArg right:noArg sleep:noArg ". +"source:noArg stop:noArg tools:noArg ttx_mix:noArg tv:noArg tv_mode:noArg up:noArg volumeDown:noArg volumeUp:noArg yellow:noArg statusRequest:noArg ". +"0_text_line 0_macro ". +"G_AUTO_:AUTO_ARC_ANTENNA_AIR,AUTO_ARC_ANTENNA_CABLE,AUTO_ARC_ANTENNA_SATELLITE,AUTO_ARC_ANYNET_AUTO_START,AUTO_ARC_ANYNET_MODE_OK,". +"AUTO_ARC_AUTOCOLOR_FAIL,AUTO_ARC_AUTOCOLOR_SUCCESS,AUTO_ARC_CAPTION_ENG,AUTO_ARC_CAPTION_KOR,AUTO_ARC_CAPTION_OFF,AUTO_ARC_CAPTION_ON,". +"AUTO_ARC_C_FORCE_AGING,AUTO_ARC_JACK_IDENT,AUTO_ARC_LNA_OFF,AUTO_ARC_LNA_ON,AUTO_ARC_PIP_CH_CHANGE,AUTO_ARC_PIP_DOUBLE,AUTO_ARC_PIP_LARGE,". +"AUTO_ARC_PIP_LEFT_BOTTOM,AUTO_ARC_PIP_LEFT_TOP,AUTO_ARC_PIP_RIGHT_BOTTOM,AUTO_ARC_PIP_RIGHT_TOP,AUTO_ARC_PIP_SMALL,AUTO_ARC_PIP_SOURCE_CHANGE,". +"AUTO_ARC_PIP_WIDE,AUTO_ARC_RESET,AUTO_ARC_USBJACK_INSPECT,AUTO_FORMAT,AUTO_PROGRAM ". +"G_EXTx:EXT1,EXT2,EXT3,EXT4,EXT5,EXT6,EXT7,EXT8,EXT9,EXT10,EXT11,EXT12,EXT13,EXT14,EXT15,EXT16,EXT17,EXT18,EXT19,EXT20,EXT21,EXT22,EXT23,". +"EXT24,EXT25,EXT26,EXT27,EXT28,EXT29,EXT30,EXT31,EXT32,EXT33,EXT34,EXT35,EXT36,EXT37,EXT38,EXT39,EXT40,EXT41 ". +"G_Others:3SPEED,4_3,16_9,ADDDEL,ALT_MHP,ANGLE,ANTENA,ANYNET,ANYVIEW,APP_LIST,AV3,BACK_MHP,BOOKMARK,CALLER_ID,CAPTION,CATV_MODE,". +"CLOCK_DISPLAY,CONVERT_AUDIO_MAINSUB,CUSTOM,DEVICE_CONNECT,DISC_MENU,DMA,DNET,DNIe,DNSe,DOOR,DSS_MODE,DTV,DTV_LINK,DTV_SIGNAL,". +"DVD_MODE,DVI,DVR,DVR_MENU,DYNAMIC,ENTERTAINMENT,FACTORY,FAVCH,FF_,FM_RADIO,GAME,HDMI3,HDMI4,ID_INPUT,ID_SETUP,INSTANT_REPLAY,LINK,". +"LIVE,MAGIC_BRIGHT,MAGIC_CHANNEL,MDC,MIC,MORE,MOVIE1,MS,MTS,NINE_SEPERATE,OPEN,PANNEL_CHDOWN,PANNEL_CHUP,PANNEL_ENTER,PANNEL_MENU,". +"PANNEL_POWER,PANNEL_SOURCE,PANNEL_VOLDOW,PANNEL_VOLUP,PANORAMA,PCMODE,PERPECT_FOCUS,PICTURE_SIZE,". +"PIP_SCAN,PIP_SIZE,PIP_SWAP,PLUS100,POWER,PRINT,QUICK_REPLAY,REC,REPEAT,RESERVED1,REWIND_,RSS,RSURF,SCALE,SEFFECT,". +"SETUP_CLOCK_TIMER,SOUND_MODE,SOURCE,SRS,STANDARD,STB_MODE,STILL_PICTURE,SUB_TITLE,SVIDEO1,SVIDEO2,SVIDEO3,TOPMENU,TTX_SUBFACE,". +"TURBO,VCHIP,VCR_MODE,WHEEL_LEFT,WHEEL_RIGHT,W_LINK,ZOOM1,ZOOM2,ZOOM_IN,ZOOM_MOVE,ZOOM_OUT "; + + if($hash->{Port} ne "8000" && $hash->{Port} ne "8001") { + my $rc = eval + { + require Net::Address::IP::Local; + require IO::Interface::Simple; + Net::Address::IP::Local->import(); + IO::Interface::Simple->import(); + 1; + }; + if($rc) { + $hash->{MyIP} = getIP(); + $hash->{MAC} = getMAC4IP($hash->{MyIP}); + } + else{ + Log3 $args[0], 3, "[SamsungAV] $args[0] You are using a deprecated MAC detection mechanism using ifconfig."; + Log3 $args[0], 3, "[SamsungAV] $args[0] Please install Pearl Modules libnet-address-ip-local-perl and libio-interface-perl"; + my $system = $^O; + my $result = ""; + if($system =~ m/Win/) { + $result = `ipconfig /all`; + my @myarp=split(/\n/,$result); + foreach (@myarp){ + if ( /([0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2}[:-][0-9a-f]{2})$/i ) { + $result = $1; + $result =~ s/-/:/g; + } + } + } + if($system eq "linux") { + $result = `ifconfig -a eth0`; + my @myarp=split(/\n/,$result); + foreach (@myarp){ + if ( /(ether|lan|eth0) .*(..:..:..:..:..:..) .*$/ ) { + $result = $2; + } + } + } + # Fritzbox "? (192.168.0.1) at 00:0b:5d:91:fc:bb [ether] on lan" + # debian "192.168.0.1 ether c0:25:06:1f:3c:14 C eth0" + #$result = "? (192.168.0.1) at 00:0b:5d:91:fc:bb [ether] on lan"; + + $hash->{MAC} = $result; + $hash->{MyIP} = getIP_old(); + } + } + else { + eval {require JSON; 1;} + or do { + return "[SamsungAV] Define: Module JSON not installed. Run 'sudo apt-get install libjson-perl'"; + }; + eval {require Time::HiRes; 1;} + or do { + return "[SamsungAV] Define: Module Time:HiRes not installed. Check perl libraries'"; + }; +# import Time::HiRes qw(usleep); + } + my $dev = $hash->{DeviceName}; +} +else { + $hash->{".validcommands"} = "mute:on,off volume:slider,0,1,100 call sms date "; +} + + $hash->{".validcommands"} .= "caller:noArg " if (defined(AttrVal($args[0], "callerURI", undef))); + $hash->{".validcommands"} .= "screen:noArg " if (defined(AttrVal($args[0], "screenURI", undef))); + + Log3 $args[0], 3, "[SamsungAV] $args[0] defined with host: $hash->{Host} port: $hash->{Port}"; + readingsSingleUpdate($hash,"state","defined",1); + return undef; +} + +sub SamsungAV_Undefine($$) +{ + my ($hash,$arg) = @_; + RemoveInternalTimer($hash); + return undef; +} + +sub SamsungAV_Attr(@) +{ + my @a = @_; + my $hash = $defs{$a[1]}; + my $cmd = $a[0]; + my $attrName = $a[2]; + my $attrParm = $a[3]; + my $mac = AttrVal($a[1], "MAC", undef); + $hash->{MAC} = $mac if (defined($mac)); + if( defined(index($attrName,"URI"))) { + if( $cmd eq "set" ) { + $hash->{".validcommands"} .= "caller:noArg " if ($attrName eq "callerURI"); + $hash->{".validcommands"} .= "screen:noArg " if ($attrName eq "screenURI"); + } + } + + if($cmd eq "set" && $attrName eq "disable") { + if($attrParm eq "0") { + readingsSingleUpdate($hash, "state", "defined",0) if(exists($hash->{helper}{DISABLED}) && $hash->{helper}{DISABLED} == 1); + SamsungAV_Init($hash); + } + elsif($attrParm eq "1") { + RemoveInternalTimer($hash); + readingsSingleUpdate($hash, "presence", "absent",1); + readingsSingleUpdate($hash, "state", "disabled",1); + } + $hash->{helper}{DISABLED} = $attrParm; + } + elsif($cmd eq "del" && $attrName eq "disable") { + readingsSingleUpdate($hash, "state", "defined",0) if(exists($hash->{helper}{DISABLED}) && $hash->{helper}{DISABLED} == 1); + SamsungAV_Init($hash); + $hash->{helper}{DISABLED} = 0; + } + + return undef; +} + +sub SamsungAV_Init($) +{ + my ($hash) = @_; + + SamsungAV_State($hash); + + RemoveInternalTimer($hash, "SamsungAV_Init"); + InternalTimer(gettimeofday()+60, "SamsungAV_Init", $hash, 0); + +} +sub SamsungAV_State($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + my $port = $hash->{Port}; + my $param = { + url => "http://$hash->{Host}:$port/", + timeout => 5, + hash => $hash, + method => "GET", + header => "", + callback => \&SamsungAV_ParseState + }; + + HttpUtils_NonblockingGet($param); +} + +sub SamsungAV_ParseState($) +{ + my ($param, $err, $data) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; +# Log3 $name, 1, "[SamsungAV] $name parameter - code: $param->{code} - error: $err"; + my ($state, $stateAV, $presence) = ""; + $param->{code} = "" if(!defined($param->{code})); + if($err ne "" && index($err,"empty") < 0) { # workaround for C/D/E/F-Series still to figure out http server Port 7676(ne "" $param->{code} ne "40") + Log3 $name, 4, "[SamsungAV] $name not able to connect to $hash->{Host}:$hash->{Port} with $param->{url} - code: $param->{code} - error: $err"; + $state = $stateAV = $presence = "absent"; + } + else { + Log3 $name, 4, "[SamsungAV] $name online with $hash->{Host}:$hash->{Port} - HTTP-Response: $param->{code}"; + $state = $stateAV = "on"; + $presence = "present"; + } + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged($hash, "state", $state,1); + + if (defined($hash->{Mode})) { + my $mute = ReadingsVal($hash->{Mode},"mute","not found"); + if($mute eq "1") {$mute = "on"} + elsif ($mute eq "0") {$mute = "off"} + if ($presence ne "absent") { + my $DLNAstate = ReadingsVal($hash->{Mode},"state","not found"); + if ($mute eq "on") { $stateAV = "muted" } + elsif ($DLNAstate ne "online") { $stateAV = $DLNAstate } + } + readingsBulkUpdateIfChanged($hash, "stateAV", $stateAV); + readingsBulkUpdateIfChanged($hash, "presence", $presence); + readingsBulkUpdateIfChanged($hash, "mute", $mute); + readingsBulkUpdateIfChanged($hash, "volume", ReadingsVal($hash->{Mode},"volume","not found")); + readingsBulkUpdateIfChanged($hash, "friendlyName", ReadingsVal($hash->{Mode},"friendlyName","not found")); + readingsBulkUpdateIfChanged($hash, "modelName", ReadingsVal($hash->{Mode},"modelName","not found")); + } + readingsEndUpdate($hash, 1); + } + +sub SamsungAV_Notify($$) +{ + my ($hash,$dev) = @_; + + return undef if(!defined($hash) or !defined($dev)); + + my $name = $hash->{NAME}; + my $dev_name = $dev->{NAME}; + + return undef if(!defined($dev_name) || !defined($name)); + + my $events = deviceEvents($dev,1); + + if($dev_name eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})) { + if(!exists($hash->{helper}{DISABLED}) || $hash->{helper}{DISABLED} == 0) { + Log3 $name, 3, "[SamsungAV] device $name initialising...."; + SamsungAV_Init($hash); + SamsungAV_Tizen_App_list($hash) if ($hash->{Port} ne "8000"); + } + } +} + +sub SamsungAV_Ready($) +{ + my ($hash) = @_; + if(AttrVal($hash->{NAME},'fork','disable') eq 'enable') { + } + else { + Log3 $hash->{NAME}, 5, "[SamsungAV] $hash->{NAME} SamsungAV_Ready(connection seems to be closed) OpenDev for DeviceName: $hash->{DeviceName}"; + } + return undef; +} + +sub SamsungAV_SetState($$$$) +{ + my ($hash, $tim, $vt, $val) = @_; + $val = $1 if($val =~ m/^(.*) \d+$/); +# return "Undefined value $val" if(!defined($it_c2b{$val})); + return undef; +} + +sub SamsungAV_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3 $name, 3, "[SamsungAV] $name SamsungAV_Read: connection DeviceName $hash->{DeviceName} may be closed"; +} + +sub SamsungAV_Set($@) +{ + my ($hash, @a) = @_; + my $nam = shift @a; + my $name = $hash->{NAME}; + my $Port = $hash->{Port}; + my $Mode = $hash->{Mode}; + my $cmd = (defined($a[0]) ? shift @a : ""); #command + my $par = (defined($a[0]) ? shift @a : ""); #1 parameter + my %translate = ("channelDown" => "CHDOWN", + "channelUp" => "CHUP", + "channelList" => "CH_LIST", + "volumeDown" => "VOLDOWN", + "volumeUp" => "VOLUP" ); + + if ($cmd eq "?" || $cmd eq "") { + return $hash->{".validcommands"}; + } + + $cmd = $par if (substr($cmd,0,2) eq "G_" ); + + if ($hash->{".validcommands"} =~ /$cmd/) { + $cmd = $translate{$cmd} if (defined($translate{$cmd})); + if ($cmd eq "statusRequest") { + SamsungAV_Tizen_App_list($hash) if ($hash->{Port} ne "8000"); + return SamsungAV_State($hash); # query connection state + } + # screen message via DLNA caller or individual + return "$name currently not available. Try later. " if (ReadingsVal($name,"state","not found") ne "on" && !($cmd eq "power" || $cmd eq "poweron" )); +# screen message, absolute volume, TTS via DLNARenderer + if (defined $Mode) { + my $command = ""; + if ($cmd eq "caller" || + $cmd eq "screen") { + my $URI =""; + if ( $cmd ne "caller" ) {$URI = AttrVal($name, "screenURI", undef)} + else {$URI = AttrVal($name, "callerURI", undef)} + $command = "set ".$hash->{Mode}." stream ".$URI; + readingsSingleUpdate($hash, "currentMedia", $URI, 1); + } + elsif ($cmd eq "volume") { + $command = "set ".$hash->{Mode}." volume ".$par; + } + elsif ($cmd eq "sayText") { + $par = '"'.$par.' '.join(" ", @a).'"'; + $command = "set ".$hash->{Mode}." speak ".$par; + } + if ($command ne "") { + Log3 $name, 4, "[SamsungAV] $name call DLNA-Renderer. Command: $command"; + return SamsungAV_DLNA($hash,$command) + } + } + + if ($cmd eq "channel") { + if($par < 10){$cmd = $par} + else { + $cmd = "0_macro"; + $par = substr($par,0,1). "," . substr($par,1,1); + } + } +# App functionality for Samsung Tizen since K-Series + return SamsungAV_Tizen_App($hash,$name,$cmd,$par) if (substr($cmd,0,5) eq "0_App" ); +# RC commands + if ($Port eq 55000 ){ + return SamsungAV_55000($hash,$name,$cmd,$par); + } + elsif ($Port eq 52235 ){ + return SamsungAV_52235($hash,@_); + } + elsif ($Port eq 8001 ){ + return SamsungAV_Tizen_RC($hash,$name,$cmd,$par); + } + elsif ($Port eq 8000 ){ + return SamsungAV_Tizen_RC($hash,$name,$cmd,$par); + } + } + else { + my $ret = "[SamsungAV] Invalid command $cmd. Use any of:\n"; + my @cmds = split(" ",$hash->{".validcommands"}); + foreach my $line (0..$#cmds) { + $ret .= "\n" if ($line > 1 && $line/10 == int($line/10)); + $ret .= $cmds[$line]." "; + } + return $ret; + } + return undef; +} + +# Samsung Tizen Models +sub SamsungAV_Tizen_RC($$$$) +{ + my ($hash,$name,$cmd,$par) = @_; + + Log3 $name, 5, "[SamsungAV] $name command ".$cmd. " parameter ".$par; + + my $dev = $hash->{Host}; + my $wsport = $hash->{Port}; + my ($payload); + + if($cmd ne "0_macro") {@ARGV = split(" ",$cmd)} + else {@ARGV = split(",",$par)} + +# screen message caller or individual via Webbrowser +if ( $cmd eq "caller" || + $cmd eq "screen") { + return SamsungAV_7676($hash,$cmd) if ($wsport ne "8001"); + return SamsungAV_Tizen_App($hash, $name,$cmd,"Internet"); +} + +(my $msg, my $socket) = &SamsungAV_Tizen_websocket_open($hash,$name); + +if ($msg ne "101") {return $msg} +else { + syswrite $socket, build_frame (0, 1, 0, 0, 0, 0x1, '1::/com.samsung.companion') if ($wsport != 8001); + eval {require Time::HiRes; 1;} + or do { + return "[SamsungAV] Module Time:HiRes not installed. Check perl libraries"; + }; + foreach my $argnum (0 .. $#ARGV) { + if ($argnum > 0) { + Time::HiRes::usleep(AttrVal($name, 'delayMacro', 300000)); + } + else {Time::HiRes::usleep(AttrVal($name, 'delayRC', 0)); } + if ($ARGV[$argnum] ne "") { + # Send remote key(s) + Log3 $name, 4, "[SamsungAV] $name sending ".uc($ARGV[$argnum]); + if ($wsport != 8001) { + $payload = $hash->{".commands"}[$argnum]; + } + else { + $payload = JSON::encode_json({ + "method" => "ms.remote.control", + "params" => { + "Cmd" => "Click", + "DataOfCmd" => "KEY_".uc($ARGV[$argnum]), + "Option" => "false", + "TypeOfRemote" => "SendRemoteKey" + } + }); + } + SamsungAV_Tizen_write_payload($socket, $payload, $name); +# BlockingCall("SamsungAV_execSamsungCtl", $name."|".$hash->{Host}."|".$ARGV[$argnum]); + } + else {Log3 $name, 4, "[SamsungAV] $name sending pause"} + } + SamsungAV_Tizen_websocket_close($socket); +} + +return undef; +} + +# Samsung Tizen open websocket +sub SamsungAV_Tizen_websocket_open($$) +{ + my ($hash,$name) = @_; + + my $dev = $hash->{Host}; + my $port = $hash->{Port}; + my ($msg,$webclient_header,$path,$socket); + my $token = ReadingsVal($name,".token",undef); # (my $error, my $token) = getKeyValue($name."token"); # + if ($port != 8001) { +# $hash->{Port} = 8443; + $path = SamsungAV_Tizen_Encrypt_Init($hash,$name); +# $hash->{Port} = $port; + return $path if (substr($path,0,11) eq "[SamsungAV]"); + $webclient_header = "GET " . $path ." HTTP/1.1\r\n"; + $socket = IO::Socket::INET->new(PeerAddr=>"$dev:$port", Timeout=>2, Blocking=>1, ReuseAddr=>1); +# $port = 8443; # secure port 8002 since K-series, 8443 for H-/J-Series + Log3 $name, 4, "[SamsungAV] HTTP socket-connection to $name. Reply: ". $socket->error(); + } + else { + eval {require IO::Socket::SSL; 1;} + or do { + return "[SamsungAV] Set: Module IO::Socket::SSL not installed. Check perl libraries"; + }; +# $webclient_header = "GET /api/v2/channels/samsung.remote.control?name=RkhFTVJlbW90ZQ== HTTP/1.1\r\n"; + $webclient_header = "GET /api/v2/channels/samsung.remote.control?name=RkhFTVJlbW90ZQ=="; +# $webclient_header .= "&token=$hash->{token}" if (defined($hash->{token})) ; #temporary + readingsSingleUpdate($hash, ".token", $hash->{token},0) if (defined($hash->{token})); #temporary + $webclient_header .= "&token=$token" if (defined($token)); + $webclient_header .= " HTTP/1.1\r\n"; + $port = 8002; # secure port 8002 since K-series, 8443 for H-/J-Series + $IO::Socket::SSL::DEBUG = 3 if(AttrVal($name,"verbose","0") eq "5"); + $IO::Socket::SSL::DEBUG = 2 if(AttrVal($name,"verbose","0") eq "4"); + $socket = IO::Socket::SSL->new(PeerAddr=>"$dev:$port", SSL_verify_mode=>0,Timeout=>2, Blocking=>1, ReuseAddr=>1); + Log3 $name, 4, "[SamsungAV] HTTP socket-connection to $name. SSL_Reply: ". IO::Socket::SSL::errstr(); + $IO::Socket::SSL::DEBUG = 0; # stop detailed debug lines + } + + if($socket) { + Log3 $name, 4, "[SamsungAV] HTTP socket-connection to $name successful."; + $webclient_header .= "Upgrade: websocket" . "\r\n" . + "Connection: Upgrade" . "\r\n" . + "Host: $dev:$port" . "\r\n" . + # "Origin: $dev:$port" . "\r\n" . + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" . "\r\n" . + "Sec-WebSocket-Version: 13" . "\r\n\r\n"; + Log3 $name, 5, "[SamsungAV] $name send to TV: " . $webclient_header; + my $written = syswrite $socket, $webclient_header; + Log3 $name, 3, "[SamsungAV] $name header written with error $!" if (!defined($written)); + my $read = sysread $socket, my $buf, 10240; + Log3 $name, 3, "[SamsungAV] $name read response with error $!" if (!defined($read)); + Log3 $name, 5, "[SamsungAV] $name first websocket response: $buf"; + $buf =~ /^[^ ]+ ([\d]{3})/; + if (!defined($1)) { + $msg = "[SamsungAV] HTTP websocket-connection to $name not successful; undefined response $buf of device"; + Log3 $name, 2, $msg; + return $msg; + } + my $sysbytes =""; + $buf = ""; + if ($port != 8000) { + do { + $sysbytes .= $buf; + $read = sysread $socket, $buf, 1; + Log3 $name, 3, "[SamsungAV] $name read response with error $!" if (!defined($read)); + } until $buf eq "{"; + Log3 $name, 5, "[SamsungAV] $name Statusbytes of second websocket response: " . unpack('H*', $sysbytes); + } + $read = sysread $socket, $buf, 10240; + Log3 $name, 3, "[SamsungAV] $name read response with error $!" if (!defined($read)); + $buf = "{" . $buf; + Log3 $name, 5, "[SamsungAV] $name data of second websocket response: $buf"; +# $read = sysread $socket, $buf, 3; +# Log3 $name, 3, "[SamsungAV] $name data of third read websocket error response: $!, data: ".unpack('H*', $buf)." , bytecount = $read"; +# $read = sysread $socket, $buf, 10240; +# Log3 $name, 3, "[SamsungAV] $name data of fourth read websocket response: $buf, bytecount = $read"; + if ($1 eq '101') { + if ($port != 8000) { + +# my $line = '{"data":{"clients":[{"attributes":{"name":"RkhFTVJlbW90ZQ==","token":"14576607"},"connectTime":1542049383707,"deviceName":"RkhFTVJlbW90ZQ==","id":"c376a63e-61aa-4e73-b72e-7cb26596f9e7","isHost":false}],"id":"c376a63e-61aa-4e73-b72e-7cb26596f9e7","token":"11142450"},"event":"ms.channel.connect"}'; + my $json = eval{JSON::decode_json($buf)}; + if (defined($json->{data}->{token})) { +# $hash->{token} = $json->{data}->{token}; #temporary + readingsSingleUpdate($hash, ".token", $json->{data}->{token},0); # setKeyValue($name."token", $json->{data}->{token}); # + Log3 $name, 5, "[SamsungAV] $name token: $json->{data}->{token} saved"; + } + } + if ($buf =~ /timeOut/) { + $msg = "[SamsungAV] $name connected but authorization timed out. Didn't you see the pop-up on TV ? "; + } + elsif ($buf =~ /unauthorized/) { + $msg = "[SamsungAV] $name connected but authorization failed. Check authorizations of TV"; + } + elsif ($buf =~ /touchDisabled/) { # Trs received events ms.remote.touchDisable after committing new authorization request + $msg = "[SamsungAV] $name connected but authorization failed as touchDisabled. Check authorizations of TV. Token deleted."; + readingsDelete($hash, ".token") if (defined($token)); + } + else {return ("101", $socket);} + Log3 $name, 3, $msg; + SamsungAV_Tizen_websocket_close($socket); + return $msg; + } + + else { + $msg = "[SamsungAV] HTTP websocket-connection to $name not successful; invalid response to websocket upgrade request"; + Log3 $name, 2, $msg; + return $msg; + } + } + else { + $msg = "[SamsungAV] HTTP socket-connection to $name not successful. "; + $msg .= "SSL_Error: ". IO::Socket::SSL::errstr() if ($port != 8000); + # $msg = "[SamsungAV] HTTP socket-connection to $name not successful. SSL_Error: ". $IO::SOCKET::SSL::SSL_Error . " SSL_Error: " .$SSL_Error . " IO_Error: " .$socket->error(); + Log3 $name, 2, $msg; + return $msg; + } + +} +# Samsung Tizen close websocket +sub SamsungAV_Tizen_websocket_close($) +{ + my ($socket) = @_; + syswrite $socket, build_frame (0, 1, 0, 0, 0, 0x8, ''); + shutdown $socket, 2; +} + +# Samsung Tizen write_payload +sub SamsungAV_Tizen_write_payload($$$) +{ + my ($socket, $payload, $name) = @_; + syswrite $socket, build_frame (0, 1, 0, 0, 0, 0x1, $payload); + Log3 $name, 5, "[SamsungAV] $name send payload: " . $payload; +} + +# Initialization of H-, J-Series with encrypted websocket access +sub SamsungAV_Tizen_Encrypt_Init($$) +{ + my ($hash,$name) = @_; + + my $dev = $hash->{Host}; + my $wsport = $hash->{Port}; + + my $msg = SamsungAV_Tizen_Encrypt($hash,$name); + return $msg if (defined($msg)); + + my $millis = int ([gettimeofday] * 1000); + my ($err, $ret) = HttpUtils_BlockingGet ({ + url => "http://$dev:$wsport/socket.io/1/?t=$millis", + method => 'GET' + }); + +if( defined($err) && $err ) { + $msg = "[SamsungAV] $name: Error getting websocket url"; + Log3 $name, 2, $msg; + return $msg; +} + +unless ($ret =~ m/:websocket,/i) { + $msg = "[SamsungAV] $name: Content error in websocket url $err"; + Log3 $name, 2, $msg; + return $msg; +} + +my $websocket_url = "ws://$dev:$wsport/socket.io/1/websocket/" . (split(':', $ret))[0]; + +unless ( $websocket_url =~ /^ws:\/\/([^:\/]+):(\d+)(\/.+)$/ ) { + $msg = "[SamsungAV] $name: Error websocket url $websocket_url"; + Log3 $name, 2, $msg; + return $msg; +} + +my ($host, $port, $path) = ($1, $2, $3); + +Log3 $name, 4, "[SamsungAV] $name: websocket path $path"; + +return $path; +} +# command encryption of H-, J-Series +sub SamsungAV_Tizen_Encrypt($$) +{ + my ($hash,$name) = @_; + + my $dev = $hash->{Host}; + my $msg; + + my $KEYfile = $hash->{NAME} . "_session_key.txt"; + +#==================================================================================== +#Read key and session-id from file, previously created with regapp.pl + +my $json; my $enc_key; my $line; my $session = ''; + +unless ( -e $KEYfile) { + $KEYfile = "samsung_session_key.txt"; +} + +if (open (KEY, $KEYfile)) { + $line = ; + close (KEY); +} +else { + $msg = "[SamsungAV] $name: ERROR cannot open file for input the session key."; + Log3 $name, 1, $msg; + return $msg; +} +eval {require Crypt::Rijndael; 1;} +or do { + $msg = "[SamsungAV] $name: Module Crypt::Rijndael not installed. Run 'sudo apt-get install libcrypt-rijndael-perl'"; + Log3 $name, 1, $msg; + return $msg; +}; + + +if (JSON::decode_json($line)) { + $json = JSON::decode_json($line); + $enc_key = $json->{session_key}; Log3 $name, 5, "session_key: ".$enc_key; + $session = $json->{session_id}; Log3 $name, 5, "session_id: ".$session; +} +else { + $msg = "[SamsungAV] $name: ERROR: $line is no JSON."; + Log3 $name, 1, $msg; + return $msg; +} + +# unhexlify key +$enc_key =~ s/([a-fA-F0-9]{2})/chr(hex($1))/eg; + +my ($cipher, $command_bytes, @chars, $int_array, $command); +undef $hash->{".commands"}; +foreach (@ARGV) { # generate command array + if ($_ ne "") { + $_ = 'KEY_' . uc($_) if ( $_ !~ m/^KEY_/ ); + Log3 $name, 5, "[SamsungAV] $name: generate command for key '$_'"; + # generate json + $json = '{"method":"POST","body":{"plugin":"RemoteControl","param1":"uuid:12345","param2":"Click","param3":"' . $_ . '","param4":false,"api":"SendRemoteKey","version":"1.000"}}'; + # encrypt json + $json = $json . chr(16 - length($json)%16) x ( 16 - length($json)%16 ); # padding + $cipher = Crypt::Rijndael->new( $enc_key, Crypt::Rijndael::MODE_ECB() ); + $command_bytes = $cipher->encrypt($json); + # generate command + @chars = $command_bytes =~ /./sg; foreach (@chars) { $_ = ord($_); } + $int_array = join (',', @chars); + $command = '5::/com.samsung.companion:{"name":"callCommon","args":[{"Session_Id":' . $session . ',"body":"[' . $int_array . ']"}]}'; + } + else {$command = $_} + Log3 $name, 5, "[SamsungAV] $name: command: '$command'"; + push @{$hash->{".commands"}} , $command; # save command to array +} +return undef; +} + +sub build_frame { + + my ($masked, $fin, $rsv1, $rsv2, $rsv3, $op, $payload) = @_; + + # Head + my $head = $op + ($fin ? 128 : 0); + $head |= 0b01000000 if $rsv1; + $head |= 0b00100000 if $rsv2; + $head |= 0b00010000 if $rsv3; + my $frame = pack 'C', $head; + + # Small payload + my $len = length $payload; + if ($len < 126) { + $frame .= pack 'C', $masked ? ($len | 128) : $len; + } + + # Extended payload (16-bit) + elsif ($len < 65536) { + $frame .= pack 'Cn', $masked ? (126 | 128) : 126, $len; + } + + # Extended payload (64-bit with 32-bit fallback) + else { + $frame .= pack 'C', $masked ? (127 | 128) : 127; + $frame .= pack('NN', 0, $len & 0xffffffff); + #$frame .= MODERN ? pack('Q>', $len) : pack('NN', 0, $len & 0xffffffff); + } + + # Mask payload + if ($masked) { + my $mask = pack 'N', int(rand 9 x 7); + $payload = $mask . xor_encode($payload, $mask x 128); + } + + return $frame . $payload; +} + +# Samsung Tizen Model App list +sub SamsungAV_Tizen_App_list($) +{ + my ($hash) = @_; + my $dev = $hash->{Host}; + my $port = $hash->{Port}; + my $name = $hash->{NAME}; + + return undef if ($hash->{STATE} ne "on"); + + my ($msg, $payload, $webclient_header, $socket, $appnames); + + ($msg, $socket) = &SamsungAV_Tizen_websocket_open($hash,$name); + + if ($msg ne "101") {return $msg} + else { + $payload = JSON::encode_json({ + "method" => "ms.channel.emit", + "params" => { + "event" => "ed.installedApp.get", + "to" => "host", + "TypeOfRemote" => "SendRemoteKey" + } + }); + SamsungAV_Tizen_write_payload($socket, $payload, $name); + sysread $socket, my $buf, 4; # read hex header first + Time::HiRes::usleep(500000); # just to get all data, otherwise crashing FHEM + sysread $socket, $buf, 10240; + Log3 $name, 5, "[SamsungAV] response $name to write_payload: $buf"; + SamsungAV_Tizen_websocket_close($socket); + my $json = eval {decode_json($buf)}; + + if ( ref($json->{data}{data}) eq "ARRAY" ) { + Log3 $name, 5, "[SamsungAV] ARRAY found"; + foreach my $rec (@{$json->{data}{data}}) { +# Log3 $name, 3, "[SamsungAV] Application: $rec"; +# Log3 $name, 3, "[SamsungAV] Application: $rec->appId"; + Log3 $name, 5, '[SamsungAV] Application: ' . $rec->{"name"} . ' Id: ' . $rec->{"appId"}; + $rec->{"name"} =~ tr/A-Za-z0-9#.-_//cd; # remove spaces an other characters + $appnames .= $rec->{"name"} . ","; + $hash->{helper}{app}{$rec->{"name"}} = $rec->{"appId"}; + } + $hash->{".validcommands"} .= "0_App_start:" . $appnames . " 0_App_state:" . $appnames . " "; + } + else { + Log3 $name, 3, "[SamsungAV] $name timelag to reach all json data for app list might be too small"; + } +} + +return undef; +} +# Samsung Tizen Model App commands +sub SamsungAV_Tizen_App($$$$) +{ + my ($hash, $name, $cmd, $par) = @_; + my $dev = $hash->{Host}; + my $port = $hash->{Port}; + my ($msg, $payload, $socket,$method,$action_type); + if (substr($cmd,0,5) eq "0_App" && #App command, otherwise internal call(e.g. caller, screen...) + substr($cmd,9,1) ne "r") { #not start command + if (substr($cmd,9,1) ne "p") { #status command + $method = "GET" + } + else {$method = "GET"} #stop command + my $param = {url => "http://$dev:$port/ws/apps/$par", + timeout => 5, + # hash => $hash, + method => $method, + header => "", + # callback => \&SamsungAV_Tizen_App_callback + }; + # HttpUtils_NonblockingGet($param); + my ($err, $data) = HttpUtils_BlockingGet($param); + if ($method eq "GET") { + my $stateapp; + if ($data =~ m/running/) {$stateapp = "running"} + else {$stateapp = "stopped"} + $msg = "[SamsungAV] $name: state of app $par: $stateapp"; + Log3 $name, 4, $msg; + return $msg; + } + } + else { + # $method = "POST"} #start command ; only useable for selected Apps(Netflix, YouTube) to be used with simple http_request + ($msg, $socket) = &SamsungAV_Tizen_websocket_open($hash,$name); + if ($msg ne "101") {return $msg} + else { +# if ($hash->app_type ne "4") {$action_type = "DEEP_LINK"} +# else {$action_type = "NATIVE_LAUNCH"} + my $URI = ""; + if ($par ne "Internet") {$action_type = "DEEP_LINK"} + else { + if ( $cmd eq "caller" ) {$URI = AttrVal($name, "callerURI", undef)} + elsif ( $cmd eq "screen" ) {$URI = AttrVal($name, "screenURI", undef)} + Log3 $name, 4, "[SamsungAV] $name browser access with URI: $URI"; + $action_type = "NATIVE_LAUNCH" + } + $payload = JSON::encode_json({ + "method" => "ms.channel.emit", + "params" => { + "event" => "ed.apps.launch", + "to" => "host", + "data" => { + "appId" => $hash->{helper}{app}{$par}, +### "appId" => "org.tizen.browser", # app_type 4 --> $action_type = "NATIVE_LAUNCH"; with link in metaTag possible +## "appId" => "3201710015067", # app_type 1 --> $action_type = "DEEP_LINK"; Universal Guide +## "appId" => "3201710015016", # app_type 1 --> $action_type = "DEEP_LINK"; SmartThings +## "appId" => "3201710015037", # app_type 1 --> $action_type = "DEEP_LINK"; Gallery +# "appId" => "111299001912", # app_type 2 --> $action_type = "DEEP_LINK"; YouTube +# "name" => "YouTube", #doesn't work + "action_type" => $action_type, + "metaTag" => $URI + }, + "TypeOfRemote" => "SendRemoteKey" + } + }); + SamsungAV_Tizen_write_payload($socket, $payload, $name); + sysread $socket, my $buf, 10240; + Log3 $name, 5, "[SamsungAV] response $name to write_payload: $buf"; + SamsungAV_Tizen_websocket_close($socket); + } + } +return undef; +} + +# old Samsung Models +sub SamsungAV_52235($@) +{ + my ($hash, @a) = @_; + my $name = $hash->{NAME}; + my $TV = $hash->{Host}; + my $count = @a; + my $arg = lc($a[2]); # mute volume + my $cont1 = ucfirst($arg); # Mute + my $cont2 = ""; + my $cont3 = ""; + my $cont4 = ""; + my $cont5 = ""; + my $cont6 = ""; + my $cont7 = ""; + my $cont8 = ""; + my $cont9 = ""; + + if (defined $a[3]) { $cont2 = $a[3]} + if (defined $a[4]) { $cont3 = $a[4]} + if (defined $a[5]) { $cont4 = $a[5]} + if (defined $a[6]) { $cont5 = $a[6]} + if (defined $a[7]) { $cont6 = $a[7]} + if (defined $a[8]) { $cont7 = $a[8]} + if (defined $a[9]) { $cont8 = $a[9]} + + my $head = ""; + my $callsoap = "\r\n". + "\r\n". + "\r\n"; + + my $kind = 0; + + if ( $arg eq "mute" ) + { + $kind = 1; + if ( $cont2 eq "off" ){ + $cont2 = 0 ; + }else { + $cont2 = 1 ; + } + } + if ( $arg eq "volume") + { + if ( $cont2 > 0 and $cont2 < 100 ){ + $kind = 1; + }else { + Log3 $name, 3, "[SamsungAV] $name Volume: not correct"; + $kind = 0; + } + } + if ( $kind eq 1){ + $callsoap .= "\r\n"; + $callsoap .= "0\r\n"; + $callsoap .= "$cont2\r\n"; + $callsoap .= "Master\r\n"; + $callsoap .= "\r\n"; + + $head .= "POST /upnp/control/RenderingControl1 HTTP/1.1\r\n"; + $head .= "Content-Type: text/xml; charset=\"utf-8\"\r\n"; + $head .= "SOAPACTION: \"SoapAction:urn:schemas-upnp-org:service:RenderingControl:1#Set$cont1\"\r\n"; + } + else { + my $body = ""; + my $operator = ""; + my $calldate=`date +"%Y-%m-%d"`; + chomp($calldate); + my $calltime=`date +"%H:%M:%S"`; + chomp($calltime); +# if ( $arg ne "del") +# { + $operator = "Add"; +# } +# else +# { +# $kind = 5; +# $operator = "Remove"; +# } + $head .= "POST /PMR/control/MessageBoxService HTTP/1.1\r\n"; + $head .= "Content-Type: text/xml; charset=\"utf-8\"\r\n"; + $head .= "SOAPACTION: \"urn:samsung.com:service:MessageBoxService:1#".$operator."Message\"\r\n"; + $callsoap .= "\r\n"; + $callsoap .= "text/xml\r\n"; + $callsoap .= "1334799348\r\n"; + $callsoap .= "\r\n"; + if ( $arg eq "call") + { + $kind = 2; + $callsoap .= "<Category>Incoming Call</Category>\r\n"; + $callsoap .= "<DisplayType>Maximum</DisplayType>\r\n"; + $callsoap .= "<CallTime>\r\n"; + $callsoap .= "<Date>$calldate</Date>\r\n"; + $callsoap .= "<Time>$calltime</Time>\r\n"; + $callsoap .= "</CallTime>\r\n"; + $callsoap .= "<Callee>\r\n"; + $callsoap .= "<Name>An: $cont4</Name>\r\n"; + $callsoap .= "<Number>Nr: $cont5</Number>\r\n"; + $callsoap .= "</Callee>\r\n"; + $callsoap .= "<Caller>\r\n"; + $callsoap .= "<Name>Von: $cont2</Name>\r\n"; + $callsoap .= "<Number>Nr: $cont3</Number>\r\n"; + $callsoap .= "</Caller>\r\n"; + } + if ( $arg eq "sms") + { + $kind = 3; + for my $i (6..$count){ + $body .= $a[$i]; + $body .= " "; + } + $callsoap .= "<Category>SMS</Category>\r\n"; + $callsoap .= "<DisplayType>Maximum</DisplayType>\r\n"; + $callsoap .= "<ReceiveTime>\r\n"; + $callsoap .= "<Date>$calldate</Date>\r\n"; + $callsoap .= "<Time>$calltime</Time>\r\n"; + $callsoap .= "</ReceiveTime>\r\n"; + $callsoap .= "<Receiver>\r\n"; + $callsoap .= "<Name>An: $cont4</Name>\r\n"; + $callsoap .= "<Number>Nr: $cont5</Number>\r\n"; + $callsoap .= "</Receiver>\r\n"; + $callsoap .= "<Sender>\r\n"; + $callsoap .= "<Name>Von: $cont2</Name>\r\n"; + $callsoap .= "<Number>Nr: $cont3</Number>\r\n"; + $callsoap .= "</Sender>\r\n"; + $callsoap .= "<Body>Inhalt: $body</Body>\r\n"; + } + if ( $arg eq "date") + { + $kind = 4; + for my $i (10..$count){ + $body .= $a[$i]; + $body .= " "; + } + $callsoap .= "<Category>Schedule Reminder</Category>\r\n"; + $callsoap .= "<DisplayType>Maximum</DisplayType>\r\n"; + $callsoap .= "<StartTime>\r\n"; + $callsoap .= "<Date>$cont2</Date>\r\n"; + $callsoap .= "<Time>$cont3</Time>\r\n"; + $callsoap .= "</StartTime>\r\n"; + $callsoap .= "<Owner>\r\n"; + $callsoap .= "<Name>Fr: $cont4</Name>\r\n"; + $callsoap .= "<Number>Nr: $cont5</Number>\r\n"; + $callsoap .= "</Owner>\r\n"; + $callsoap .= "<Subject>Betreff: $cont6</Subject>\r\n"; + $callsoap .= "<EndTime>\r\n"; + $callsoap .= "<Date>$cont7</Date>\r\n"; + $callsoap .= "<Time>$cont8</Time>\r\n"; + $callsoap .= "</EndTime>\r\n"; + $callsoap .= "<Location>Ort: $cont9</Location>\r\n"; + $callsoap .= "<Body>Inhalt: $body</Body>\r\n"; + } + $callsoap .= "\r\n"; + $callsoap .= "\r\n"; + } + if ( $kind ne 0 ){ + return soap_call($hash,$head,$callsoap,52235); + }else{ + return "Unknown argument, choose one of mute:on,off volume:slider,0,1,100 call sms date"; + } +} + +# new Samsung Models +sub SamsungAV_55000($$$$) +{ + my ($hash,$name,$cmd,$par) = @_; + + if($cmd ne "0_macro") {@ARGV = split(" ",$cmd)} + else {@ARGV = split(",",$par)} + #### Configuration + my $tv = "UE46ES8090"; # Might need changing to match your TV type #"UE46ES8090" + my $port = $hash->{Port}; # TCP port of Samsung TV + my $tvip = $hash->{Host}; # IP Address of TV #"192.168.2.124" + my $myip = $hash->{MyIP}; # IP Address of FHEM Server + my $mymac = $hash->{MAC}; # Used for the access control/validation '"24:65:11:80:0D:01" + +# screen message caller or individual +if ( $cmd eq "caller" || + $cmd eq "screen") { + return SamsungAV_7676($hash,$cmd); +} + my $msg; + my $appstring = "iphone..iapp.samsung"; # What the iPhone app reports + my $tvappstring = "iphone.".$tv.".iapp.samsung"; # TV type + my $remotename = "Perl Samsung Remote"; # What gets reported when it asks for permission/also shows in General->Wireless Remote Control menu + + #### MAC überprüfen wenn nicht gültig vom attribute übernehmen. + if ($mymac !~ /^\w\w:\w\w:\w\w:\w\w|\w\w:\w\w:\w\w:\w\w$/) { + Log3 $name, 3, "[SamsungAV] mymac: $mymac invalid format"; + }else{ + # command-line help + if (!$tv|!$tvip|!$myip|!$mymac) { + return "[SamsungAV] Error - Parameter missing:\nmodel, tvip, myip, mymac."; + } + + Log3 $name, 5, "[SamsungAV] $name: opening socket with tvip: $tvip, cmd: $cmd"; + my $sock = new IO::Socket::INET ( + PeerAddr => $tvip, + PeerPort => $port, + Proto => 'tcp', + Timout => 5 + ); + + if (defined ($sock)){ + my $messagepart1 = chr(0x64) . chr(0x00) . chr(length(encode_base64($myip, ""))) . chr(0x00) . encode_base64($myip, "") . chr(length(encode_base64($mymac, ""))) . chr(0x00) . encode_base64($mymac, "") . chr(length(encode_base64($remotename, ""))) . chr(0x00) . encode_base64($remotename, ""); + my $part1 = chr(0x00) . chr(length($appstring)) . chr(0x00) . $appstring . chr(length($messagepart1)) . chr(0x00) . $messagepart1; + print $sock $part1; + + my $messagepart2 = chr(0xc8) . chr(0x00); + my $part2 = chr(0x00) . chr(length($appstring)) . chr(0x00) . $appstring . chr(length($messagepart2)) . chr(0x00) . $messagepart2; + print $sock $part2; + # Preceding sections all first time only + + if ($cmd eq "0_text_line") { + # Send text, e.g. in YouTube app's search, N.B. NOT BBC iPlayer app. + my $text = $par; + my $messagepart3 = chr(0x01) . chr(0x00) . chr(length(encode_base64($text, ""))) . chr(0x00) . encode_base64($text, ""); + my $part3 = chr(0x01) . chr(length($appstring)) . chr(0x00) . $appstring . chr(length($messagepart3)) . chr(0x00) . $messagepart3; + print $sock $part3; + } + else { + foreach my $argnum (0 .. $#ARGV) { + Time::HiRes::usleep(300000) if ($argnum > 0); + if ($ARGV[$argnum] ne "") { + # Send remote key(s) +# Log3 $name, 3, "[SamsungAV] sending ".uc($ARGV[$argnum]); + my $key = "KEY_" . uc($ARGV[$argnum]); + my $messagepart3 = chr(0x00) . chr(0x00) . chr(0x00) . chr(length(encode_base64($key, ""))) . chr(0x00) . encode_base64($key, ""); + my $part3 = chr(0x00) . chr(length($tvappstring)) . chr(0x00) . $tvappstring . chr(length($messagepart3)) . chr(0x00) . $messagepart3; + print $sock $part3; + Log3 $name, 4, "[SamsungAV] $name: command: '$key'"; + # select(undef, undef, undef, 0.5); + } + } + } + + close($sock); + }else{ + $msg = "Could not create socket. Port: $port. Aborting."; + Log3 $name, 2, "[SamsungAV] $name: $msg"; + } + } + return $msg; +} + +sub SamsungAV_7676($$) +{ + my ($hash,$arg) = @_; + my $TV = $hash->{Host}; + my $name = $hash->{NAME}; + my ($URI, $location) = undef; + + if ($hash->{Port} eq "8000") {$location = "smp_4_" } + else {$location = "smp_12_" } + + my $callsoap .= "\r\n"; + $callsoap .= "\r\n"; + $callsoap .= "\r\n"; + $callsoap .= "\r\n"; + if ( $arg ne "caller" ) {$URI = AttrVal($name, "screenURI", undef)} + else {$URI = AttrVal($name, "callerURI", undef)} + if (defined $URI) {$callsoap .= "".$URI."\r\n"} + $callsoap .= "\r\n"; + + my $head .= "POST /$location HTTP/1.1\r\n"; + $head .= "Content-Type: text/xml; charset=\"utf-8\"\r\n"; + $head .= "SOAPACTION: \"urn:samsung.com:service:MainTVAgent2:1#RunBrowser\"\r\n"; + + return soap_call($hash,$head,$callsoap,7676); +} +sub soap_call($$$$) +{ + my ($hash,$head,$callsoap,$port) = @_; + my $name = $hash->{NAME}; + my $TV = $hash->{Host}; + my $msg; + + $callsoap .= "\r\n"; + $callsoap .= "\r\n"; + + my $size = length($callsoap); + + $head .= "Cache-Control: no-cache\r\n"; + $head .= "Host: $TV:$port\r\n"; + $head .= "Content-Length: $size\r\n"; + $head .= "Connection: Close\r\n"; + $head .= "\r\n"; + + my $buffer = ""; + my $tmp = $head . $callsoap; + my @tmp2 = ""; + + Log3 $name, 4, "[SamsungAV] $name: $TV:$port connection message: $tmp"; + + my $sock = new IO::Socket::INET ( + PeerAddr => $TV, + PeerPort => $port, + Proto => 'tcp', + Timout => 5 + ); + if (defined ($sock)){ + print $sock $tmp; + my $buff =""; + while ((read $sock, $buff, 1) > 0){ + $buffer .= $buff; + } + Log3 $name, 2, "[SamsungAV] $name $TV: empty answer received" if ($buffer eq ""); + @tmp2 = split (/\n/,$buffer); + Log3 $name, 4, "[SamsungAV] $name $TV: socket answer $buffer"; + $sock->close(); + Log3 $name, 4, "[SamsungAV] $name $TV: socket closed"; + }else{ + $msg = "Could not create socket. Port: $port Aborting."; + Log3 $name, 2, "[SamsungAV] $name $TV: $msg"; + } + return $msg; +} +sub SamsungAV_DLNA($$) +{ + my ($hash,$command) = @_; + my $name = $hash->{NAME}; + + if (ReadingsVal($hash->{Mode},"state","offline") ne "offline") { + # copying of picture for caller to be done outside of program + fhem($command); + Log3 $name, 5, "[SamsungAV] $name DLNA-command: $command"; + } + else { + return "[SamsungAV] $name DLNAdevice $hash->{Mode} currently offline"; + } +} + +sub getIP() +{ + my $address = eval {Net::Address::IP::Local->public_ipv4}; + if ($@) { + $address = 'localhost'; + } + return "$address"; +} + +sub getIP_old() +{ + my $host = hostname(); + my $address = inet_ntoa(scalar gethostbyname(hostname() || 'localhost')); + if ($@) { + $address = 'localhost'; + } + return "$address"; +} + +sub getMAC4IP($) +{ + my $IP = shift; + my @interfaces = IO::Interface::Simple->interfaces; + foreach my $if (@interfaces) { + next unless defined ($if->address); + if ($if->address eq $IP) { + return $if->hwaddr; + } + } + return ""; +} +# Callback from 95_remotecontrol for command makenotify. +# Param1: Name of remoteControl device +# Param2: Name of target FHEM device +sub SamsungAV_RCmakenotify($$) { + my ($name, $ndev) = @_; + my $nname="notify_$name"; + + fhem("define $nname notify $name set $ndev remoteControl ".'$EVENT',1); + Log3 undef, 2, "[remoteControl:SamsungAV] Notify created: $nname"; + return "Notify created by SamsungAV: $nname"; +} + +# Callback from 95_remotecontrol for command layout. Creates non svg layout +sub SamsungAV_RClayout_TV() { + my @row; + my $i = 0; + + $row[$i++]="poweroff:POWEROFF, :blank, source:SOURCE"; + $row[$i++]=":blank, hdmi:HDMI, :blank"; + $row[$i++]="1, 2, 3"; + $row[$i++]="4, 5, 6"; + $row[$i++]="7, 8, 9"; + $row[$i++]="ttx_mix:TEXT, 0, prech:PRECH"; +# $row[$i++]=":blank, :blank, :blank"; + $row[$i++]="volumeUp:UP, mute:MUTE, channelUp:CHUP"; + $row[$i++]=":VOL, :blank, :PROG"; + $row[$i++]="volumeDown:DOWN, channelList:CH_LIST,channelDown:CHDOWN"; + $row[$i++]="menu:MENU, :blank, guide:GUIDE"; +# $row[$i++]=":blank, :blank, :blank"; + $row[$i++]="tools:TOOLS, up:UP, info:INFO"; + $row[$i++]="left:LEFT, enter:ENTER, right:RIGHT"; + $row[$i++]="reurn:RETURN, down:DOWN, exit:EXIT"; + $row[$i++]="red:RED, green:GREEN, yellow:YELLOW, cyan:BLUE"; + $row[$i++]=":blank, :SEARCH, :blank"; + $row[$i++]=":EGUIDE, aspect:PICTURE_SIZE, ad:AD"; + $row[$i++]="rewind:REWIND, pause:PAUSE, ff:FF"; + $row[$i++]="rec:REC, play:PLAY, stop:STOP"; + + # unused available commands + # SOURCE PIP_ONOFF + # CONTENTS W_LINK + # RSS MTS SRS CAPTION TOPMENU SLEEP ESAVING + + #Remove spaces + for (@row) {s/\s+//g} + + $row[$i++]="attr rc_iconpath icons/remotecontrol"; + $row[$i++]="attr rc_iconprefix black_btn_"; + + return @row; +} + +# Callback from 95_remotecontrol for command layout. Creates svg layout +sub SamsungAV_RClayout_TV_SVG() { + my @row; + my $i = 0; + + $row[$i++]="poweroff:rc_POWER.svg, :rc_BLANK.svg, tv:rc_TV.svg"; + $row[$i++]=":rc_BLANK.svg, hdmi:rc_HDMI.svg, :rc_BLANK.svg"; + $row[$i++]="1:rc_1.svg, 2:rc_2.svg, 3:rc_3.svg"; + $row[$i++]="4:rc_4.svg, 5:rc_5.svg, 6:rc_6.svg"; + $row[$i++]="7:rc_7.svg, 8:rc_8.svg, 9:rc_9.svg"; + $row[$i++]="ttx_mix:rc_TEXT.svg, 0:rc_0.svg, prech:rc_PREVIOUS.svg"; + $row[$i++]=":rc_BLANK.svg, :rc_BLANK.svg, :rc_BLANK.svg"; + $row[$i++]="volumeUp:rc_UP.svg, mute:rc_MUTE.svg, channelUp:rc_UP.svg"; + $row[$i++]=":rc_VOL.svg, :rc_BLANK.svg, :rc_PROG.svg"; + $row[$i++]="volumeDown:rc_DOWN.svg, channelList:rc_PROG.svg, channelDown:rc_DOWN.svg"; + $row[$i++]="menu:rc_MENU.svg, :rc_BLANK.svg, guide:rc_EPG.svg"; + $row[$i++]=":rc_BLANK.svg, :rc_BLANK.svg, :rc_BLANK.svg"; + $row[$i++]="tools:rc_OPTIONS.svg, up:rc_UP.svg, info:rc_INFO.svg"; + $row[$i++]="left:rc_LEFT.svg, enter:rc_OK.svg, right:rc_RIGHT.svg"; + $row[$i++]="reurn:rc_RETURN.svg, down:rc_DOWN.svg, exit:rc_EXIT.svg"; + $row[$i++]="red:rc_RED.svg, green:rc_GREEN.svg, yellow:rc_YELLOW.svg, cyan:rc_BLUE.svg"; + $row[$i++]=":rc_BLANK.svg, :rc_SEARCH.svg, :rc_BLANK.svg"; + $row[$i++]=":rc_HELP.svg, aspect:rc_PICTURE_SIZE.svg, ad:rc_AD.svg"; + $row[$i++]="rewind:rc_REW.svg, pause:rc_PAUSE.svg, ff:rc_FF.svg"; + $row[$i++]="rec:rc_REC.svg, play:rc_PLAY.svg, stop:rc_STOP.svg"; + + #Remove spaces + for (@row) {s/\s+//g} + + return @row; +} + +1; + +=pod +=item summary device to communicate with Samsung AV devices +=begin html + + + +

SamsungAV

+

    +This module supports Samsung devices like TV or BD players.
    + +

    + Define
    + define <name> SamsungAV <host> <port> [DLNA]
    +

    +(B|C|D Series) use port 52235 for screen messages: call, sms, date and volume change
    +(C|D|E|F Series) use port 55000 for RC commands
    +(H|J Series) use port 8000 for RC commands
    +(newest Series) use port 8001 for RC commands
    +(all Series)The device name of optional DLNA client might be defined to send full screen messages(OSD)
    + + Example:
    + define Television SamsungAV 192.168.178.20 52235
    + define Television SamsungAV 192.168.178.20 55000
    + define Television SamsungAV 192.168.178.20 55000 DLNA_Client
    +

    + Set
    + Port 52235 functionality: + set <name> <command> [arguments]
    where command is one of:
    +
      +
    • mute
    • +
    • volume
    • +
    • call
    • +
    • sms
    • +
    • date
    • +
    + Example:
    + set Television mute on
    + set Television volume 20
    + set Television call Peter 012345678 Phone 87654321

    + RC functionality:
    + set <name> <RC command> [arguments]
    + Example:
    + set Television channelDown

    + Special commands:
    + caller/screen
    + send message(URI defined by attr callerURI/screenURI) via DLNA to device
    + Example:
    + set Television caller
    +
    + sayText
    + TTS-function, if optional DLNA-device is defined
    + Example:
    + set Television sayText das ist ein test
    + see commandref of DLNA-device for further information.
    +
    + 0_macro
    + macro function
    + send several comma separated RC commands to device(each command will cause a pause of delayMacro microsec.)
    + set <name> 0_macro <RC command1> [,RC command2,RC command3....]
    + Example:
    + set Television 0_macro contents,right,right,,down,enter
    + some TVs need different delays between RC commands in a macro. You may solve this with usage of multiple commas.
    +
    + 0_text_line
    + sent text to device(migh be helpful to submit data used by interactive apps)
    + set <name> 0_text_line <TEXT>
    + Example:
    + set Television 0_text_line http://192.168.178.1:8083/fhem/btip/info.html
    +
    + 0_App_start Netflix/YouTube (0_App_status Netflix/YouTube)
    + start app on TV (get status of app)
    +
    + Get
    +
      N/A

    +
    + Attributes
    +
  • callerURI: path to an URI of a media(jpg,mp3...)to be displayed on screen + main purpose: URI to be displayed on screen if a phone call comes in
  • +
    +
  • screenURI: path to an URI of a media(jpg,mp3...)to be displayed on screen + every other purpose than a phone call as event
  • +
    +
  • delayRC: delay in microsec. default=0 + some TV's need a delay before transmition of a RC command. + be careful: attribute causes system freezes
  • +
    +
  • delayMacro: delay in microsec. default=300000 + most TV's need a delay between each RC command in macro function. + be careful: attribute causes system freezes
  • +
    +
  • disable: 0/1: enables/disables device
  • +
+ +=end html +=cut