diff --git a/fhem/CHANGED b/fhem/CHANGED index e38cbbf8d..4d0a5b594 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. + - added: 70_BRAVIA: new module for Sony Televisions - feature: 98_statistics: attr specialDeltaPeriod (new) enhanced for more than one value and different periods - bugfix: 00_SIGNALduino: Replaced firmware with an old one. diff --git a/fhem/FHEM/70_BRAVIA.pm b/fhem/FHEM/70_BRAVIA.pm new file mode 100644 index 000000000..65877f635 --- /dev/null +++ b/fhem/FHEM/70_BRAVIA.pm @@ -0,0 +1,2067 @@ +# $Id$ +############################################################################## +# +# 70_BRAVIA.pm +# An FHEM Perl module for controlling Sony Televisons +# via network connection. Supported are models with release date starting from 2011. +# +# Copyright by Ulf von Mersewsky +# e-mail: umersewsky at gmail.com +# +# This file is part of fhem. +# +# Fhem 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 +# (at your option) any later version. +# +# Fhem 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. +# +# You should have received a copy of the GNU General Public License +# along with fhem. If not, see . +# +############################################################################## + +package main; + +use 5.012; +use strict; +use warnings; +use Data::Dumper; +use Time::HiRes qw(gettimeofday); +use Time::Local; +use HttpUtils; +use SetExtensions; +use Encode; +use JSON qw(decode_json); +use MIME::Base64; +use XML::Simple; +use IO::Socket; + +sub BRAVIA_Set($@); +sub BRAVIA_Get($@); +sub BRAVIA_GetStatus($;$); +sub BRAVIA_Define($$); +sub BRAVIA_Undefine($$); + +######################### +# Forward declaration for remotecontrol module +#sub BRAVIA_RClayout_TV(); +#sub BRAVIA_RCmakenotify($$); + +################################### +sub BRAVIA_Initialize($) { + my ($hash) = @_; + + Log3 $hash, 5, "BRAVIA_Initialize: Entering"; + + $hash->{GetFn} = "BRAVIA_Get"; + $hash->{SetFn} = "BRAVIA_Set"; + $hash->{DefFn} = "BRAVIA_Define"; + $hash->{UndefFn} = "BRAVIA_Undefine"; + + $hash->{AttrList} = "disable:0,1 macaddr:textField channelsMax:textField " . $readingFnAttributes; + + $data{RC_layout}{BRAVIA_SVG} = "BRAVIA_RClayout_SVG"; + $data{RC_layout}{BRAVIA} = "BRAVIA_RClayout"; + + $data{RC_makenotify}{BRAVIA} = "BRAVIA_RCmakenotify"; + + return; +} + +##################################### +sub BRAVIA_GetStatus($;$) { + my ( $hash, $update ) = @_; + my $name = $hash->{NAME}; + my $interval = $hash->{INTERVAL}; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_GetStatus()"; + + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + $interval, "BRAVIA_GetStatus", $hash, 0 ); + + return if ( AttrVal($name, "disable", 0) == 1 ); + + # check device availability + if (!$update) { + BRAVIA_SendCommand( $hash, "getStatus", "xml" ) + if (ReadingsVal($name, "requestFormat", "xml") eq "xml"); + BRAVIA_SendCommand( $hash, "getStatus", "json" ) + if (ReadingsVal($name, "requestFormat", "json") eq "json"); + } + + return; +} + +################################### +sub BRAVIA_Get($@) { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $what; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_Get()"; + + return "argument is missing" if ( int(@a) < 2 ); + + $what = $a[1]; + + if ( $what =~ /^(power|input|volume|mute)$/ ) { + if ( defined( $hash->{READINGS}{$what}{VAL} ) ) { + return $hash->{READINGS}{$what}{VAL}; + } + else { + return "no such reading: $what"; + } + } + + else { + return +"Unknown argument $what, choose one of power:noArg input:noArg volume:noArg mute:noArg"; + } +} + +################################### +sub BRAVIA_Set($@) { + my ( $hash, @a ) = @_; + my $name = $hash->{NAME}; + my $state = ReadingsVal($name, "state", ""); + my $channel = ReadingsVal($name, "channel", ""); + my $channelId = ReadingsVal($name, "channelId", ""); + my $channels = ""; + my $inputs = ""; + my $mutes = "toggle"; + + if ( ReadingsVal($name, "input", "") ne "-" ) { + $hash->{helper}{lastInput} = ReadingsVal($name, "input", ""); + } elsif ( !defined( $hash->{helper}{lastInput} ) ) { + $hash->{helper}{lastInput} = ""; + } + + my $input = $hash->{helper}{lastInput}; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_Set()"; + + return "No Argument given" if ( !defined( $a[1] ) ); + + # Input handling + my @inputs; + if ( defined( $hash->{helper}{device}{inputPreset} ) + && ref( $hash->{helper}{device}{inputPreset} ) eq "HASH" ) { + @inputs = keys %{ $hash->{helper}{device}{inputPreset} }; + } + @inputs = sort(@inputs); + $inputs = join(",", @inputs); + + # load channel list + my @channels; + if ( defined( $hash->{helper}{device}{channelPreset} ) + && ref( $hash->{helper}{device}{channelPreset} ) eq "HASH" ) + { + my $count = 0; + my @keys = keys %{ $hash->{helper}{device}{channelPreset} }; + @keys = sort(@keys); + my $maxChannels = (@keys < AttrVal($name, "channelsMax", 50) ? @keys : AttrVal($name, "channelsMax", 50)); + for (my $i = 0; $i < $maxChannels; $i++) { + my $preset = $keys[$i]; + if ( $hash->{helper}{device}{channelPreset}{$preset}{name} + && $hash->{helper}{device}{channelPreset}{$preset}{name} ne "" + && $hash->{helper}{device}{channelPreset}{$preset}{name} ne "-" + && $hash->{helper}{device}{channelPreset}{$preset}{id} ne "-" ) { + push( + @channels, + $hash->{helper}{device}{channelPreset}{$preset}{id}.":".$hash->{helper}{device}{channelPreset}{$preset}{name}); + } + } + } + if ( $channel ne "" && $channel ne "-" && $channelId ne "-" ) { + my $currentChannel = $channelId . ":" . $channel; + my @matches = grep("/".$currentChannel."/", @channels); + push( @channels, $currentChannel ) if ( ( scalar @matches ) eq "0" ); + } + @channels = sort(@channels); + $channels = join(",", @channels); + + $mutes .= ",on,off"; + #$mutes .= ",off" if ( defined( $hash->{READINGS}{generation}{VAL} ) and $hash->{READINGS}{generation}{VAL} ne "1.0" ); + + my $usage = "Unknown argument " . $a[1] . ", choose one of"; + $usage .= " requestFormat:json,xml register"; + $usage .= ":noArg" + if (ReadingsVal($name, "requestFormat", "") eq "xml"); + $usage .= " statusRequest:noArg toggle:noArg on:noArg off:noArg tvpause:noarg play:noArg pause:noArg stop:noArg record:noArg upnp:on,off volume:slider,1,1,100 volumeUp:noArg volumeDown:noArg channelUp:noArg channelDown:noArg remoteControl"; + $usage .= " mute:" . $mutes; + $usage .= " input:" . $inputs if ( $inputs ne "" ); + $usage .= " channel:$channels" if ( $channels ne "" ); + + my $cmd = ''; + my $result; + + # statusRequest + if ( lc( $a[1] ) eq "statusrequest" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + delete $hash->{helper}{device} + if ( defined( $hash->{helper}{device} ) ); + + BRAVIA_GetStatus($hash); + } + + # toggle + elsif ( $a[1] eq "toggle" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state ne "on" ) { + return BRAVIA_Set( $hash, $name, "on" ); + } + else { + return BRAVIA_Set( $hash, $name, "off" ); + } + + } + + # on + elsif ( $a[1] eq "on" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state ne "on" ) { + my $macAddr = AttrVal( $name, "macaddr", "" ); + $macAddr = ReadingsVal( $name, "macAddr", "") if ($macAddr eq ""); + if ( $macAddr ne "" && $macAddr ne "-" ) { + $result = BRAVIA_wake( $name, $macAddr ); + return "wake-up command sent"; + } else { + $cmd = "POWER"; + BRAVIA_SendCommand( $hash, "ircc", $cmd ); + } + } + } + + # off + elsif ( $a[1] eq "off" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state ne "absent" ) { + if ( ReadingsVal($name, "generation", "") ne "1.0" ) { + $cmd = "STANDBY"; + } else { + $cmd = "POWER"; + } + BRAVIA_SendCommand( $hash, "ircc", $cmd ); + } + else { + return "Device needs to be reachable to toggle standby mode."; + } + } + + # volume + elsif ( $a[1] eq "volume" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + + return "No argument given" if ( !defined( $a[2] ) ); + + my $vol = $a[2]; + if ( $state eq "on" ) { + if ( $vol =~ m/^\d+$/ && $vol >= 1 && $vol <= 100 ) { + $cmd = 'setVolume:' . $vol; + } + else { + return +"Argument does not seem to be a valid integer between 1 and 100"; + } + BRAVIA_SendCommand( $hash, "upnp", $cmd ); + + readingsSingleUpdate( $hash, "volume", $a[2], 1 ) + if ( ReadingsVal($name, "volume", "") ne $a[2] ); + } + else { + return "Device needs to be ON to adjust volume."; + } + } + + # volumeUp/volumeDown + elsif ( lc( $a[1] ) =~ /^(volumeup|volumedown)$/ ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + if ( lc( $a[1] ) eq "volumeup" ) { + $cmd = "VOLUP"; + } + else { + $cmd = "VOLDOWN"; + } + BRAVIA_SendCommand( $hash, "ircc", $cmd ); + } + else { + return "Device needs to be ON to adjust volume."; + } + } + + # mute + elsif ( $a[1] eq "mute" ) { + if ( defined( $a[2] ) ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + } + else { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + } + + if ( $state eq "on" ) { + if ( !defined( $a[2] ) || $a[2] eq "toggle" ) { + $result = BRAVIA_SendCommand( $hash, "ircc", "MUTE" ); + readingsSingleUpdate( $hash, "mute", (ReadingsVal($name, "mute", "") eq "on" ? "off" : "on"), 1 ); + } + elsif ( $a[2] eq "off" ) { + #$result = BRAVIA_SendCommand( $hash, "MuteOff" ) + $result = BRAVIA_SendCommand( $hash, "upnp", "setMute:0" ); + readingsSingleUpdate( $hash, "mute", $a[2], 1 ) + if ( ReadingsVal($name, "mute", "") ne $a[2] ); + } + elsif ( $a[2] eq "on" ) { + #$result = BRAVIA_SendCommand( $hash, "MuteOn" ) + $result = BRAVIA_SendCommand( $hash, "upnp", "setMute:1" ); + readingsSingleUpdate( $hash, "mute", $a[2], 1 ) + if ( ReadingsVal($name, "mute", "") ne $a[2] ); + } + else { + return "Unknown argument " . $a[2]; + } + } + else { + return "Device needs to be ON to mute/unmute audio."; + } + } + + # remoteControl + elsif ( lc( $a[1] ) eq "remotecontrol" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + + if ( $state ne "absent" ) { + if ( !defined( $a[2] ) ) { + my $commandKeys = ""; + for ( + sort keys %{ + BRAVIA_GetRemotecontrolCommand( + "GetRemotecontrolCommands") + } + ) + { + $commandKeys = $commandKeys . " " . $_; + } + return "No argument given, choose one of" . $commandKeys; + } + + $cmd = uc( $a[2] ); + + if ( $cmd eq "MUTE" ) { + BRAVIA_Set( $hash, $name, "mute" ); + } + elsif ( $cmd eq "CHANUP" ) { + BRAVIA_Set( $hash, $name, "channelUp" ); + } + elsif ( $cmd eq "CHANDOWN" ) { + BRAVIA_Set( $hash, $name, "channelDown" ); + } + elsif ( $cmd ne "" ) { + BRAVIA_SendCommand( $hash, "ircc", $cmd ); + } + else { + my $commandKeys = ""; + for ( + sort keys %{ + BRAVIA_GetRemotecontrolCommand( + "GetRemotecontrolCommands") + } + ) + { + $commandKeys = $commandKeys . " " . $_; + } + return + "Unknown argument " + . $a[2] + . ", choose one of" + . $commandKeys; + } + } + else { + return "Device needs to be reachable to be controlled remotely."; + } + } + + # channel + elsif ( $a[1] eq "channel" ) { + if ( defined( $a[2] ) + && ReadingsVal($name, "presence", "") eq "present" + && $state ne "on" ) + { + Log3 $name, 4, "BRAVIA $name: indirect switching request to ON"; + BRAVIA_Set( $hash, $name, "on" ); + } + + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + + return + "No argument given, choose one of channel presetNumber channelName " + if ( !defined( $a[2] ) ); + + if ( $state eq "on" ) { + my $channelName = $a[2]; + if ( $channelName =~ /^(\d)(\d?)(\d?)(\d?):.*$/ ) { + BRAVIA_SendCommand( $hash, "ircc", $1, "blocking" ); + BRAVIA_SendCommand( $hash, "ircc", $2, "blocking" ) if (defined($2)); + BRAVIA_SendCommand( $hash, "ircc", $3, "blocking" ) if (defined($3)); + BRAVIA_SendCommand( $hash, "ircc", $4, "blocking" ) if (defined($4)); + } else { + return "Argument " . $channelName . " is not a valid channel name"; + } + } + else { + return + "Device needs to be reachable to switch to a specific channel."; + } + } + + # channelUp/channelDown + elsif ( lc( $a[1] ) =~ /^(channelup|channeldown)$/ ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + if ( lc( $a[1] ) eq "channelup" ) { + $cmd = "CHANUP"; + } + else { + $cmd = "CHANDOWN"; + } + BRAVIA_SendCommand( $hash, "ircc", $cmd ); + } + else { + return "Device needs to be ON to switch channel."; + } + } + + # input + elsif ( $a[1] eq "input" ) { + if ( defined( $a[2] ) + && ReadingsVal($name, "presence", "") eq "present" + && $state ne "on" ) + { + Log3 $name, 4, "BRAVIA $name: indirect switching request to ON"; + BRAVIA_Set( $hash, $name, "on" ); + } + + return "No 2nd argument given" if ( !defined( $a[2] ) ); + + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + + # Resolve input uri + my $input_uri; + if ( defined( $hash->{helper}{device}{inputPreset}{ $a[2] } ) ) { + $input_uri = $hash->{helper}{device}{inputPreset}{ $a[2] }{uri}; + } else { + return "Unknown source input '" . $a[2] . "' on that device."; + } + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "setPlayContent", $input_uri ); + + if ( ReadingsVal($name, "input", "") ne $a[2] ) { + readingsSingleUpdate( $hash, "input", $a[2], 1 ); + } + } + else { + return "Device needs to be reachable to switch input."; + } + } + + # tvpause + elsif ( $a[1] eq "tvpause" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "ircc", "TVPAUSE" ); + } + else { + return "Device needs to be ON to pause tv."; + } + } + + # pause + elsif ( $a[1] eq "pause" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "ircc", "PAUSE" ); + } + else { + return "Device needs to be ON to pause video."; + } + } + + # play + elsif ( $a[1] eq "play" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "ircc", "PLAY" ); + } + else { + return "Device needs to be ON to play video."; + } + } + + # stop + elsif ( $a[1] eq "stop" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "ircc", "STOP" ); + } + else { + return "Device needs to be ON to stop video."; + } + } + + # record + elsif ( $a[1] eq "record" ) { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + + if ( $state eq "on" ) { + BRAVIA_SendCommand( $hash, "ircc", "RECORD" ); + } + else { + return "Device needs to be ON to start instant recording."; + } + } + + # register + elsif ( $a[1] eq "register" ) { + if (defined($a[2])) { + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + BRAVIA_SendCommand( $hash, "register", $a[2] ); + } else { + Log3 $name, 2, "BRAVIA set $name " . $a[1]; + BRAVIA_SendCommand( $hash, "register" ); + } + } + + # requestFormat + elsif ( $a[1] eq "requestFormat" ) { + return "No 2nd argument given" if ( !defined( $a[2] ) ); + + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + readingsSingleUpdate( $hash, "requestFormat", $a[2], 1 ) + if ( ReadingsVal($name, "requestFormat", "") ne $a[2] ); + } + + # upnp + elsif ( $a[1] eq "upnp" ) { + return "No 2nd argument given" if ( !defined( $a[2] ) ); + + Log3 $name, 2, "BRAVIA set $name " . $a[1] . " " . $a[2]; + readingsSingleUpdate( $hash, "upnp", $a[2], 1 ) + if ( ReadingsVal($name, "upnp", "") ne $a[2] ); + } + + # return usage hint + else { + return $usage; + } + + return; +} + +################################### +sub BRAVIA_Define($$) { + my ( $hash, $def ) = @_; + my @a = split( "[ \t][ \t]*", $def ); + my $name = $hash->{NAME}; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_Define()"; + + if ( int(@a) < 3 ) { + my $msg = + "Wrong syntax: define BRAVIA []"; + Log3 $name, 4, $msg; + return $msg; + } + + $hash->{TYPE} = "BRAVIA"; + + my $address = $a[2]; + $hash->{helper}{ADDRESS} = $address; + + # use interval of 45 sec if not defined + my $interval = $a[3] || 45; + $hash->{INTERVAL} = $interval; + + # number of channels read from channellist, maximum 50 + my $channelCount = 50; + $hash->{CHANNELCOUNT} = $channelCount; + + $hash->{helper}{PORT} = { + 'IRCC' => "80", + 'SERVICE' => "80", + 'UPNP' => "52323", + }; + + $hash->{helper}{HEADER} = 'X-CERS-DEVICE-ID: fhem_remote'; + + $hash->{name} = ReadingsVal($name, "name", ""); + + $hash->{modelName} = ReadingsVal($name, "modelName", ""); + + $hash->{generation} = ReadingsVal($name, "generation", ""); + + unless ( defined( AttrVal( $name, "webCmd", undef ) ) ) { + $attr{$name}{webCmd} = 'volume:channelUp:channelDown'; + } + unless ( defined( AttrVal( $name, "devStateIcon", undef ) ) ) { + $attr{$name}{devStateIcon} = + 'on:rc_GREEN:off off:rc_YELLOW:on absent:rc_STOP:on'; + } + unless ( defined( AttrVal( $name, "icon", undef ) ) ) { + $attr{$name}{icon} = 'it_television'; + } + + # start the status update timer + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 2, "BRAVIA_GetStatus", $hash, 1 ); + + return; +} + +############################################################################################################ +# +# Begin of helper functions +# +############################################################################################################ + +################################### +sub BRAVIA_SendCommand($$;$$) { + my ( $hash, $service, $cmd, $type ) = @_; + my $name = $hash->{NAME}; + my $address = $hash->{helper}{ADDRESS}; + my $port = $hash->{helper}{PORT}; + my $header = $hash->{helper}{HEADER}; + my $timestamp = gettimeofday(); + my $data; + my $timeout; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_SendCommand()"; + + my $URL; + my $response; + my $return; + my $requestFormat = ReadingsVal($name, "requestFormat", ""); + + BRAVIA_CheckRegistration($hash) if ($service ne "register" && $service ne "getStatus"); + + if ( !defined($cmd) ) { + Log3 $name, 4, "BRAVIA $name: REQ $service"; + } + else { + Log3 $name, 4, "BRAVIA $name: REQ $service/" . urlDecode($cmd); + } + + $URL = "http://" . $address . ":"; + $header .= "\r\nCookie: auth=".ReadingsVal($name, "authCookie", "") + if (ReadingsVal($name, "authCookie", "") ne ""); + if ($service eq "ircc") { + $URL .= $port->{IRCC}; + $URL .= "/sony" + if ($requestFormat eq "json"); + $URL .= "/IRCC"; + $header .= "\r\nSoapaction: \"urn:schemas-sony-com:service:IRCC:1#X_SendIRCC\""; + $header .= "\r\nContent-Type: text/xml; charset=UTF-8"; + $cmd = BRAVIA_GetRemotecontrolCommand($cmd); + $data = BRAVIA_GetIrccRequest($cmd); + } elsif ($service eq "upnp") { + my $value; + if ($cmd =~ m/^(.+):(\d+)$/) { + $cmd = $1; + $value = $2; + } + $URL .= $port->{UPNP}; + $URL .= "/upnp/control/RenderingControl"; + $header .= "\r\nSoapaction: \"urn:schemas-upnp-org:service:RenderingControl:1#"; + $header .= ucfirst($cmd); + $header .= "\""; + $header .= "\r\nContent-Type: text/xml"; + $data = BRAVIA_GetUpnpRequest($cmd, $value); + } elsif ($service eq "register") { + my $id = "Fhem Remote"; + my $device = "fhem_remote"; + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + my $uuid = ReadingsVal($name, "registrationUUID", ""); + if (defined($cmd) && $uuid ne "") { + if ($cmd ne "renew") { + $header = "Authorization: Basic "; + $header .= encode_base64(":".$cmd,""); + } + } else { + undef $header; + $uuid = createUniqueId(); + readingsSingleUpdate($hash, "registrationUUID", $uuid, 1); + } + $URL .= "/sony/accessControl"; + $data = "{\"method\":\"actRegister\",\"params\":[{"; + $data .= "\"clientid\":\"".$id.":".$uuid."\","; + $data .= "\"nickname\":\"".$id." (".$device.")\","; + $data .= "\"level\":\"private\"},"; + $data .= "[{\"value\":\"yes\",\"function\":\"WOL\"}]],\"id\":8,\"version\":\"1.0\"}"; + } else { + $URL .= "/cers/api/register?name=".urlEncode($id)."®istrAtionType=initial&deviceId=".$device; + } + } elsif ($service eq "getStatus") { + $URL .= $port->{SERVICE}; + if ($cmd eq "xml") { + $URL .= "/cers/api/" . $service; + } else { + $URL .= "/sony/system"; + $data = "{\"method\":\"getPowerStatus\",\"params\":[],\"id\":1,\"version\":\"1.0\"}"; + } + } elsif ($service eq "getContentInformation") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/avContent"; + $data = "{\"method\":\"getPlayingContentInfo\",\"params\":[],\"id\":1,\"version\":\"1.0\"}"; + } else { + $URL .= "/cersEx/api/" . $service; + } + } elsif ($service eq "getContentCount") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/avContent"; + $data = "{\"method\":\"getContentCount\",\"params\":[{\"source\":\"" . $cmd . "\",\"type\":\"\"}],\"id\":1,\"version\":\"1.0\"}"; + } + } elsif ($service eq "getContentList") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + my $source = $cmd; + my $index = 0; + if ($cmd =~ /^(.*)\|(\d+)$/){ + $source = $1; + $index = $2; + } + $URL .= "/sony/avContent"; + $data = "{\"method\":\"getContentList\",\"params\":[{\"source\":\"".$source."\",\"type\":\"\",\"cnt\":".InternalVal($name, "CHANNELCOUNT", 50).",\"stIdx\":".$index."}],\"id\":1,\"version\":\"1.0\"}"; + } + } elsif ($service eq "getCurrentExternalInputsStatus") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/avContent"; + $data = "{\"id\":2,\"method\":\"getCurrentExternalInputsStatus\",\"version\":\"1.0\",\"params\":[]}"; + } + } elsif ($service eq "setPlayContent") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/avContent"; + $data = "{\"id\":2,\"method\":\"setPlayContent\",\"version\":\"1.0\",\"params\":[{\"uri\":\"".$cmd."\"}]}"; + } + } elsif ($service eq "setPlayTvContent") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/avContent"; + $data = "{\"id\":2,\"method\":\"setPlayTvContent\",\"version\":\"1.0\",\"params\":[{\"channel\":\"".$cmd."\"}]}"; + } + } elsif ($service eq "getScheduleList") { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/recording"; + $data = "{\"method\":\"getScheduleList\",\"params\":[{\"cnt\":100,\"stIdx\":0}],\"id\":1,\"version\":\"1.0\"}"; + } else { + $URL .= "/cersEx/api/" . $service; + } + } else { + $URL .= $port->{SERVICE}; + if ($requestFormat eq "json") { + $URL .= "/sony/system"; + $data = "{\"method\":\"".$service."\",\"params\":[],\"id\":1,\"version\":\"1.0\"}"; + } else { + $URL .= "/cers"; + if ($service =~ /^Mute.*$/) { + $URL .= "/command/".$service; + } else { + $URL .= "/api/" . $service; + } + } + } + + if ( defined( $attr{$name}{timeout} ) && $attr{$name}{timeout} =~ /^\d+$/ ) { + $timeout = $attr{$name}{timeout}; + } elsif ( $service eq "getStatus" ) { + $timeout = 7; + } else { + $timeout = 30; + } + + # send request via HTTP-POST method + Log3 $name, 5, "BRAVIA $name: POST " . $URL . " (" . urlDecode($data) . ")" + if ( defined($data) ); + Log3 $name, 5, "BRAVIA $name: GET " . $URL + if ( !defined($data) ); + Log3 $name, 5, "BRAVIA $name: header " . $header + if ( defined($header) ); + + if ( defined($type) && $type eq "blocking" ) { + my ($err, $data) = HttpUtils_BlockingGet( + { + url => $URL, + timeout => 4, + noshutdown => 1, + header => $header, + data => $data, + hash => $hash, + service => $service, + cmd => $cmd, + type => $type, + timestamp => $timestamp, + } + ); + Log3 $name, 5, "BRAVIA $name: REQ $service received err: $err data: $data "; + sleep 1; + } else { + HttpUtils_NonblockingGet( + { + url => $URL, + timeout => $timeout, + noshutdown => 1, + header => $header, + data => $data, + hash => $hash, + service => $service, + cmd => $cmd, + type => $type, + timestamp => $timestamp, + callback => \&BRAVIA_ReceiveCommand, + } + ); + } + + return; +} + +################################### +sub BRAVIA_ReceiveCommand($$$) { + my ( $param, $err, $data ) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $service = $param->{service}; + my $cmd = $param->{cmd}; + + my $newstate; + my $rc = ( $param->{buf} ) ? $param->{buf} : $param; + my $return; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_ReceiveCommand() rc: $rc err: $err data: $data "; + + readingsBeginUpdate($hash); + + # device not reachable + if ($err) { + + if ( !defined($cmd) || ref($cmd) eq "HASH" || $cmd eq "" ) { + Log3 $name, 4, "BRAVIA $name: RCV TIMEOUT $service"; + } + else { + Log3 $name, 4, + "BRAVIA $name: RCV TIMEOUT $service/" . urlDecode($cmd); + } + + # device is not reachable or + # does not even support master command for status + if ( $service eq "getStatus" ) { + BRAVIA_ClearContentInformation($hash); + $newstate = "absent"; + + if ( + ( !defined( $hash->{helper}{AVAILABLE} ) ) + or ( defined( $hash->{helper}{AVAILABLE} ) + and $hash->{helper}{AVAILABLE} eq 1 ) + ) + { + $hash->{helper}{AVAILABLE} = 0; + readingsBulkUpdate( $hash, "presence", "absent" ); + } + } + + # device behaves naughty + else { + $newstate = "on"; + + Log3 $name, 3, + "BRAVIA $name: API command '".$service."' not supported by device."; + } + } + + # data received + elsif ($data) { + + if ( + ( !defined( $hash->{helper}{AVAILABLE} ) ) + or ( defined( $hash->{helper}{AVAILABLE} ) + and $hash->{helper}{AVAILABLE} eq 0 ) + ) + { + $hash->{helper}{AVAILABLE} = 1; + readingsBulkUpdate( $hash, "presence", "present" ); + } + + if ( !defined($cmd) ) { + Log3 $name, 4, "BRAVIA $name: RCV $service"; + } + else { + Log3 $name, 4, "BRAVIA $name: RCV $service/" . urlDecode($cmd); + } + + if ( $data ne "" ) { + if ( $data =~ /^<\?xml/ ) { + my $parser = XML::Simple->new( + NormaliseSpace => 2, + KeepRoot => 0, + ForceArray => 0, + SuppressEmpty => 1 + ); + + if ( !defined($cmd) || ref($cmd) eq "HASH" || $cmd eq "" ) { + Log3 $name, 4, "BRAVIA $name: RES $service - $data"; + } + else { + Log3 $name, 4, + "BRAVIA $name: RES $service/" . urlDecode($cmd) . " - $data"; + } + + readingsBulkUpdate( $hash, "requestFormat", "xml" ) + if ( $service eq "getStatus" && ReadingsVal($name , "requestFormat", "") eq "" ); + + $return = $parser->XMLin( Encode::encode_utf8($data) ); + } + + elsif ( $data =~ /^{/ || $data =~ /^\[/ ) { + if ( !defined($cmd) || ref($cmd) eq "HASH" || $cmd eq "" ) { + Log3 $name, 4, "BRAVIA $name: RES $service - $data"; + } + else { + Log3 $name, 4, + "BRAVIA $name: RES $service/" . urlDecode($cmd) . " - $data"; + } + + readingsBulkUpdate( $hash, "requestFormat", "json" ) + if ( $service eq "getStatus" && ReadingsVal($name , "requestFormat", "") eq "" ); + + $return = decode_json( Encode::encode_utf8($data) ); + } + + elsif ( $data eq "not foundnot found" ) { + if ( !defined($cmd) || ref($cmd) eq "HASH" || $cmd eq "" ) { + Log3 $name, 4, "BRAVIA $name: RES $service - not found"; + } + else { + Log3 $name, 4, + "BRAVIA $name: RES $service/" . urlDecode($cmd) . " - not found"; + } + + $return = "not found"; + } + + elsif ( $data =~ /^{NAME}; + + Log3 $name, 5, "BRAVIA $name: called function BRAVIA_Undefine()"; + + # Stop the internal GetStatus-Loop and exit + RemoveInternalTimer($hash); + + return; +} + +################################### +sub BRAVIA_wake ($$) { + my ( $name, $mac_addr ) = @_; + my $address = '255.255.255.255'; + my $port = 9; + + my $sock = new IO::Socket::INET( Proto => 'udp' ) + or die "socket : $!"; + die "Can't create WOL socket" if ( !$sock ); + + my $ip_addr = inet_aton($address); + my $sock_addr = sockaddr_in( $port, $ip_addr ); + $mac_addr =~ s/://g; + my $packet = + pack( 'C6H*', 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, $mac_addr x 16 ); + + setsockopt( $sock, SOL_SOCKET, SO_BROADCAST, 1 ) + or die "setsockopt : $!"; + + Log3 $name, 4, + "BRAVIA $name: Waking up by sending Wake-On-Lan magic package to " + . $mac_addr; + send( $sock, $packet, 0, $sock_addr ) or die "send : $!"; + close($sock); + + return; +} + +################################### +# process return data +sub BRAVIA_ProcessCommandData ($$) { + + my ($param, $return) = @_; + my $hash = $param->{hash}; + my $name = $hash->{NAME}; + my $service = $param->{service}; + my $cmd = $param->{cmd}; + my $type = ( $param->{type} ) ? $param->{type} : ""; + my $header = $param->{httpheader}; + my $newstate; + + # ircc + if ( $service eq "ircc" ) { + if ( ref($return) ne "HASH" && $return eq "ok" ) { + + # toggle standby + if ( defined($type) && $type eq "off" ) { + $newstate = "off"; + } + + # toggle standby + elsif ( defined($type) && $type eq "on" ) { + $newstate = "on"; + } + + } + } + + # upnp + elsif ( $service eq "upnp" ) { + if ( ref($return) eq "HASH" ) { + if ( $cmd eq "getVolume" ) { + my $volume = $return->{"s:Body"}{"u:GetVolumeResponse"}{CurrentVolume}; + if ( defined( $volume ) ) { + readingsBulkUpdate( $hash, "volume", $volume ) + if (ReadingsVal($name, "volume", "-1") ne $volume); + } + } elsif ( $cmd eq "getMute" ) { + my $mute = $return->{"s:Body"}{"u:GetMuteResponse"}{CurrentMute} eq "0" ? "off" : "on"; + if ( defined( $mute ) ) { + readingsBulkUpdate( $hash, "mute", $mute ) + if (ReadingsVal($name, "mute", "-1") ne $mute); + } + } + } + } + + # getStatus + elsif ( $service eq "getStatus" ) { + my $input = "-"; + my $setInput; + + my %statusKeys; + foreach ( keys %{ $hash->{READINGS} } ) { + $statusKeys{$_} = 1 if ( $_ =~ /^s_.*/ && ReadingsVal($name, $_, "") ne "-" ); + } + if ( ref($return) eq "HASH" ) { + if ( ref($return->{status}{statusItem}) eq "ARRAY" ) { + foreach ( @{ $return->{status}{statusItem} } ) { + if ( $_->{field} eq "source" ) { + $input = $_->{value}; + $setInput = "true"; + } else { + readingsBulkUpdate( $hash, "s_".$_->{field}, $_->{value} ) + if (ReadingsVal($name, "s_".$_->{field}, "") ne $_->{value} ); + } + delete $statusKeys{"s_".$_->{field}}; + } + } elsif (defined($return->{status}{statusItem}{field})) { + my $field = "s_".$return->{status}{statusItem}{field}; + if ( defined($field) && $field ne "" ) { + if ( $field eq "s_source" ) { + $input = $return->{status}{statusItem}{value}; + $setInput = "true"; + } else { + readingsBulkUpdate( $hash, $field, $return->{status}{statusItem}{value} ) + if (ReadingsVal($name, $field, "") ne $return->{status}{statusItem}{value} ); + } + delete $statusKeys{$field}; + } + } + } + + readingsBulkUpdate( $hash, "input", $input ) + if ( defined($setInput) and + (ReadingsVal($name, "input", "") ne $input) ); + + #remove outdated content information - replaces by "-" + foreach ( keys %statusKeys ) { + readingsBulkUpdate( $hash, $_, "-" ); + } + + # check for valid status + if (ref $return eq ref {} && ref($return->{error}) eq "ARRAY" && $return->{error}[0] eq "404") { + BRAVIA_ClearContentInformation($hash); + return "off"; + } + + + # fetch other info + + # read system information if not existing + BRAVIA_SendCommand( $hash, "getSystemInformation" ) + if ( ReadingsVal($name, "name", "0") eq "0" ); + + # read content information + if ( ReadingsVal($name, "generation", "1.0") ne "1.0" ) { + if (ref $return eq ref {} && ref($return->{result}) eq "ARRAY" && $return->{result}[0]{status} ne "active") { + # current status is not active, don't need to fetch content information + BRAVIA_ClearContentInformation($hash); + $newstate = "off"; + } else { + BRAVIA_SendCommand( $hash, "getContentInformation" ); + } + } elsif (ref $return eq ref {}) { + if (ref($return->{result}) eq "ARRAY") { + $newstate = ( $return->{result}[0]{status} eq "active" ? "on" : $return->{result}[0]{status} ); + } else { + $newstate = ( $return->{status}{name} eq "viewing" ? "on" : $return->{status}{name} ); + } + # get current system settings + if ($newstate eq "on" && ReadingsVal($name, "upnp", "on") eq "on") { + BRAVIA_SendCommand( $hash, "upnp", "getVolume" ); + BRAVIA_SendCommand( $hash, "upnp", "getMute" ); + } + } + } + + # getSystemInformation + elsif ( $service eq "getSystemInformation" ) { + if ( ref($return) eq "HASH" ) { + if (ref($return->{result}) eq "ARRAY") { + my $sysInfo = $return->{result}[0]; + readingsBulkUpdate( $hash, "name", $sysInfo->{name} ); + readingsBulkUpdate( $hash, "generation", $sysInfo->{generation} ); + readingsBulkUpdate( $hash, "area", $sysInfo->{area} ); + readingsBulkUpdate( $hash, "language", $sysInfo->{language} ); + readingsBulkUpdate( $hash, "country", $sysInfo->{region} ); + readingsBulkUpdate( $hash, "modelName", $sysInfo->{model} ); + readingsBulkUpdate( $hash, "macAddr", $sysInfo->{macAddr} ); + $hash->{name} = $sysInfo->{name}; + $hash->{modelName} = $sysInfo->{model}; + $hash->{generation} = $sysInfo->{generation}; + } else { + readingsBulkUpdate( $hash, "name", $return->{name} ); + readingsBulkUpdate( $hash, "generation", $return->{generation} ); + readingsBulkUpdate( $hash, "area", $return->{area} ); + readingsBulkUpdate( $hash, "language", $return->{language} ); + readingsBulkUpdate( $hash, "country", $return->{country} ); + readingsBulkUpdate( $hash, "modelName", $return->{modelName} ); + $hash->{name} = $return->{name}; + $hash->{modelName} = $return->{modelName}; + $hash->{generation} = $return->{generation}; + } + } + } + + # getContentInformation + elsif ( $service eq "getContentInformation" ) { + my %contentKeys; + my $channelName = "-"; + my $channelNo = "-"; + my $currentTitle = "-"; + my $currentMedia = "-"; + foreach ( keys %{ $hash->{READINGS} } ) { + $contentKeys{$_} = 1 + if ( $_ =~ /^ci_.*/ and ReadingsVal($name, $_, "") ne "-" ); + } + if ( ref($return) eq "HASH" ) { + $newstate = "on"; + if ( defined($return->{infoItem}) ) { + # xml + if ( ref($return->{infoItem}) eq "ARRAY" ) { + foreach ( @{ $return->{infoItem} } ) { + if ( $_->{field} eq "displayNumber" ) { + $channelNo = $_->{value}; + } elsif ( $_->{field} eq "inputType" ) { + $currentMedia = $_->{value}; + } elsif ( $_->{field} eq "serviceName" ) { + $channelName = BRAVIA_GetNormalizedName($_->{value}); + } elsif ( $_->{field} eq "title" ) { + $currentTitle = Encode::decode_utf8($_->{value}); + } else { + readingsBulkUpdate( $hash, "ci_".$_->{field}, $_->{value} ) + if ( ReadingsVal($name, "ci_".$_->{field}, "") ne $_->{value} ); + delete $contentKeys{"ci_".$_->{field}}; + } + } + } else { + my $field = "ci_".$return->{infoItem}->{field}; + my $value = $return->{infoItem}->{value}; + readingsBulkUpdate( $hash, $field, $value ) + if ( ReadingsVal($name, $field, "") ne $value ); + delete $contentKeys{$field}; + } + } else { + # json + if ( ref($return->{result}[0]) eq "HASH" ) { + foreach ( keys %{$return->{result}[0]} ) { + if ( $_ eq "dispNum" ) { + $channelNo = $return->{result}[0]{$_}; + } elsif ( $_ eq "programMediaType" ) { + $currentMedia = $return->{result}[0]{$_}; + } elsif ( $_ eq "title" ) { + $channelName = BRAVIA_GetNormalizedName($return->{result}[0]{$_}); + } elsif ( $_ eq "programTitle" ) { + $currentTitle = Encode::decode_utf8($return->{result}[0]{$_}); + } elsif ( $_ eq "source" ) { + readingsBulkUpdate( $hash, "input", $return->{result}[0]{$_} ) + if ( ReadingsVal($name, "input", "") ne $return->{result}[0]{$_} ); + } else { + readingsBulkUpdate( $hash, "ci_".$_, $return->{result}[0]{$_} ) + if ( ReadingsVal($name, "ci_".$_, "") ne $return->{result}[0]{$_} ); + delete $contentKeys{"ci_".$_}; + } + } + } elsif ( ref($return->{error}) eq "ARRAY" && $return->{error}[0] eq "7" && $return->{error}[1] eq "Illegal State" ) { + #could be timeshift mode + BRAVIA_SendCommand( $hash, "getScheduleList" ); + return; + } + } + } else { + if ( ReadingsVal($name, "input", "") eq "Others" || ReadingsVal($name, "input", "") eq "Broadcast" ) { + $newstate = "off"; + } else { + $newstate = "on"; + } + } + readingsBulkUpdate( $hash, "channel", $channelName ) + if ( ReadingsVal($name, "channel", "") ne $channelName ); + readingsBulkUpdate( $hash, "channelId", $channelNo ) + if ( ReadingsVal($name, "channelId", "") ne $channelNo ); + readingsBulkUpdate( $hash, "currentTitle", $currentTitle ) + if ( ReadingsVal($name, "currentTitle", "") ne $currentTitle ); + readingsBulkUpdate( $hash, "currentMedia", $currentMedia ) + if ( ReadingsVal($name, "currentMedia", "") ne $currentMedia ); + + if ($channelName ne "-" && $channelNo ne "-") { + BRAVIA_SendCommand( $hash, "getContentList", ReadingsVal($name, "input", "") ) + if (ReadingsVal($name, "requestFormat", "") eq "json" + && (!defined($hash->{helper}{device}{channelPreset}) || ReadingsVal($name, "state", "") ne "on")); + $hash->{helper}{device}{channelPreset}{ $channelNo }{id} = $channelNo; + $hash->{helper}{device}{channelPreset}{ $channelNo }{name} = $channelName; + } + + #remove outdated content information - replaces by "-" + foreach ( keys %contentKeys ) { + readingsBulkUpdate( $hash, $_, "-" ); + } + + # get current system settings + if ($newstate eq "on" && ReadingsVal($name, "upnp", "on") eq "on") { + BRAVIA_SendCommand( $hash, "upnp", "getVolume" ); + BRAVIA_SendCommand( $hash, "upnp", "getMute" ); + } + + # load input list if just switched on + if ($newstate eq "on" + && (ReadingsVal($name, "state", "") ne "on" || !defined($hash->{helper}{device}{inputPreset})) + && ReadingsVal($name, "requestFormat", "") eq "json") { + BRAVIA_SendCommand( $hash, "getCurrentExternalInputsStatus" ); + } + } + + # getScheduleList + elsif ( $service eq "getScheduleList" ) { + my %contentKeys; + my $channelName = "-"; + my $currentTitle = "-"; + my $currentMedia = "-"; + foreach ( keys %{ $hash->{READINGS} } ) { + $contentKeys{$_} = 1 + if ( $_ =~ /^ci_.*/ and ReadingsVal($name, $_, "") ne "-" ); + } + if ( ref($return) eq "HASH" ) { + if (ref($return->{result}) eq "ARRAY") { + $newstate = "on"; + foreach ( @{ $return->{result} } ) { + foreach ( @{ $_ } ) { + if ($_->{recordingStatus} eq "recording") { + my $key; + foreach $key ( keys %{ $_ }) { + if ( $key eq "type" ) { + $currentMedia = $_->{$key}; + readingsBulkUpdate( $hash, "input", $_->{$key} ) + if ( ReadingsVal($name, "input", "") ne $_->{$key} ); + } elsif ( $key eq "channelName" ) { + $channelName = BRAVIA_GetNormalizedName($_->{$key}); + } elsif ( $key eq "title" ) { + $currentTitle = Encode::decode_utf8($_->{$key}); + } else { + readingsBulkUpdate( $hash, "ci_".$key, $_->{$key} ) + if ( ReadingsVal($name, "ci_".$key, "") ne $_->{$key} ); + delete $contentKeys{"ci_".$key}; + } + } + } + } + } + } + } + readingsBulkUpdate( $hash, "channel", $channelName ) + if ( ReadingsVal($name, "channel", "") ne $channelName ); + readingsBulkUpdate( $hash, "currentTitle", $currentTitle ) + if ( ReadingsVal($name, "currentTitle", "") ne $currentTitle ); + readingsBulkUpdate( $hash, "currentMedia", $currentMedia ) + if ( ReadingsVal($name, "currentMedia", "") ne $currentMedia ); + + #remove outdated content information - replaces by "-" + foreach ( keys %contentKeys ) { + readingsBulkUpdate( $hash, $_, "-" ); + } + + # get current system settings + if (ReadingsVal($name, "upnp", "on") eq "on") { + BRAVIA_SendCommand( $hash, "upnp", "getVolume" ); + BRAVIA_SendCommand( $hash, "upnp", "getMute" ); + } + } + + # getContentList + elsif ( $service eq "getContentList" ) { + my $channelIndex = 0; + if ( ref($return) eq "HASH" ) { + if (ref($return->{result}) eq "ARRAY") { + foreach ( @{ $return->{result} } ) { + foreach ( @{ $_ } ) { + my $channelNo; + my $channelName; + my $key; + foreach $key ( keys %{ $_ }) { + if ( $key eq "dispNum" ) { + $channelNo = $_->{$key}; + } elsif ( $key eq "title" ) { + $channelName = BRAVIA_GetNormalizedName($_->{$key}); + } elsif ( $key eq "index" ) { + $channelIndex = $_->{$key}; + } + } + $hash->{helper}{device}{channelPreset}{ $channelNo }{id} = $channelNo; + $hash->{helper}{device}{channelPreset}{ $channelNo }{name} = $channelName; + } + } + } + } + # increment index, because it starts with 0 + if (++$channelIndex % InternalVal($name, "CHANNELCOUNT", 50) == 0) { + # try next junk of channels + BRAVIA_SendCommand( $hash, "getContentList", ReadingsVal($name, "input", "")."|".$channelIndex ); + } + } + + # getCurrentExternalInputsStatus + elsif ( $service eq "getCurrentExternalInputsStatus" ) { + my $channelIndex = 0; + if ( ref($return) eq "HASH" ) { + if (ref($return->{result}) eq "ARRAY") { + foreach ( @{ $return->{result} } ) { + foreach ( @{ $_ } ) { + my $inputName; + my $inputUri; + my $key; + foreach $key ( keys %{ $_ }) { + if ( $key eq "uri" ) { + $inputUri = $_->{$key}; + } elsif ( $key eq "title" ) { + $inputName = BRAVIA_GetNormalizedName($_->{$key}); + } + } + $hash->{helper}{device}{inputPreset}{$inputName}{uri} = $inputUri; + } + } + $hash->{helper}{device}{inputPreset}{TV}{uri} = "tv"; + } + } + } + + # register + elsif ( $service eq "register" ) { + if ( $header =~ /auth=([A-Za-z0-9]+)/ ) { + readingsBulkUpdate( $hash, "authCookie", $1 ); + } + if ( $header =~ /[Ee]xpires=([^;]+)/ ) { + readingsBulkUpdate( $hash, "authExpires", $1 ); + } + if ( $header =~ /[Mm]ax-[Aa]ge=(\d+)/ ) { + readingsBulkUpdate( $hash, "authMaxAge", $1 ) if (ReadingsVal($name, "authMaxAge", 0) != $1); + } + } + + # all other command results + else { + Log3 $name, 2, "BRAVIA $name: ERROR: method to handle response of $service not implemented"; + } + + return $newstate; + +} + +##################################### +sub BRAVIA_ClearContentInformation ($) { + + my ($hash) = @_; + my $name = $hash->{NAME}; + + #remove outdated content information - replaces by "-" + foreach ( keys %{ $hash->{READINGS} } ) { + readingsBulkUpdate($hash, $_, "-") + if ( $_ =~ /^ci_.*/ and ReadingsVal($name, $_, "") ne "-" ); + } + + readingsBulkUpdate( $hash, "channel", "-" ) + if ( ReadingsVal($name, "channel", "") ne "-" ); + readingsBulkUpdate( $hash, "channelId", "-" ) + if ( ReadingsVal($name, "channelId", "") ne "-" ); + readingsBulkUpdate( $hash, "currentTitle", "-" ) + if ( ReadingsVal($name, "currentTitle", "") ne "-" ); + readingsBulkUpdate( $hash, "currentMedia", "-" ) + if ( ReadingsVal($name, "currentMedia", "") ne "-" ); + readingsBulkUpdate( $hash, "input", "-" ) + if ( ReadingsVal($name, "input", "") ne "-" ); + +} + + +##################################### +# Callback from 95_remotecontrol for command makenotify. +sub BRAVIA_RCmakenotify($$) { + my ( $nam, $ndev ) = @_; + my $nname = "notify_$nam"; + + fhem( "define $nname notify $nam set $ndev remoteControl " . '$EVENT', 1 ); + Log3 undef, 2, "[remotecontrol:BRAVIA] Notify created: $nname"; + return "Notify created by BRAVIA: $nname"; +} + +##################################### +# RC layouts + +# Sony TV with SVG +sub BRAVIA_RClayout_SVG() { + my @row; + + $row[0] = "SOURCE:rc_AV.svg,:rc_BLANK.svg,:rc_BLANK.svg,POWER:rc_POWER.svg"; + $row[1] = "TVPAUSE:rc_TVstop.svg,ASPECT,MODE3D,TRACKID"; + $row[2] = "PREVIOUS:rc_PREVIOUS.svg,REWIND:rc_REW.svg,FORWARD:rc_FF.svg,NEXT:rc_NEXT.svg"; + $row[3] = "REC:rc_REC.svg,PLAY:rc_PLAY.svg,PAUSE:rc_PAUSE.svg,STOP:rc_STOP.svg"; + $row[4] = "RED:rc_RED.svg,GREEN:rc_GREEN.svg,YELLOW:rc_YELLOW.svg,BLUE:rc_BLUE.svg"; + $row[5] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg"; + + $row[6] = "HELP:rc_HELP.svg,SEN,SYNCMENU"; + $row[7] = "GUIDE:rc_MENU.svg,UP:rc_UP.svg,INFO:rc_INFO.svg"; + $row[8] = "LEFT:rc_LEFT.svg,OK:rc_OK.svg,RIGHT:rc_RIGHT.svg"; + $row[9] = "RETURN:rc_BACK.svg,DOWN:rc_DOWN.svg,OPTIONS:rc_OPTIONS.svg,HOMEtxt"; + $row[10] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg"; + + $row[11] = "DIGITAL,EXIT:rc_EXIT.svg,TV:rc_TV.svg"; + $row[12] = "1:rc_1.svg,2:rc_2.svg,3:rc_3.svg"; + $row[13] = "4:rc_4.svg,5:rc_5.svg,6:rc_6.svg"; + $row[14] = "7:rc_7.svg,8:rc_8.svg,9:rc_9.svg"; + $row[15] = "TEXT:rc_TEXT.svg,0:rc_0.svg,SUBTITLE"; + $row[16] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg"; + + $row[17] = "MUTE:rc_MUTE.svg,VOLUP:rc_VOLPLUS.svg,CHANNELUP:rc_UP.svg,AUDIO:rc_AUDIO.svg"; + $row[18] = ":rc_BLANK.svg,VOLDOWN:rc_VOLMINUS.svg,CHANNELDOWN:rc_DOWN.svg"; + + $row[19] = "attr rc_iconpath icons"; + $row[20] = "attr rc_iconprefix rc_"; + return @row; +} + +# Sony TV with PNG +sub BRAVIA_RClayout() { + my @row; + + $row[0] = "SOURCE,:blank,:blank,POWER:POWEROFF"; + $row[1] = "TVPAUSE:TVstop,ASPECT,MODE3D,TRACKID"; + $row[2] = "PREVIOUS,REWIND,FORWARD:FF,NEXT"; + $row[3] = "REC,PLAY,PAUSE,STOP"; + $row[4] = "RED,GREEN,YELLOW,BLUE"; + $row[5] = ":blank,:blank,:blank"; + + $row[6] = "HELP,SEN,SYNCMENU"; + $row[7] = "GUIDE,UP,INFO"; + $row[8] = "LEFT,OK,RIGHT"; + $row[9] = "RETURN,DOWN,OPTIONS:SUBMENU,HOMEtxt"; + $row[10] = ":blank,:blank,:blank"; + + $row[11] = "DIGITAL,EXIT,TV"; + $row[12] = "1,2,3"; + $row[13] = "4,5,6"; + $row[14] = "7,8,9"; + $row[15] = "TEXT,0,SUBTITLE"; + $row[16] = ":blank,:blank,:blank"; + + $row[17] = "MUTE,VOLUP:VOLUP2,CHANNELUP:CHUP2,AUDIO"; + $row[18] = ":blank,VOLDOWN:VOLDOWN2,CHANNELDOWN:CHDOWN2"; + return @row; +} + +################################### +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# 755 +# +# +# +# 755 +# +# 755 +# +# +# +# +# 755 +# 755 +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# +# 755 +# 755 +# 755 +# +# +# 755 +# 755 +# 755 +# 755 +# 755 +# 755 +# 755 +# 755 +sub BRAVIA_GetRemotecontrolCommand($) { + my ($command) = @_; + my $commands = { + 'POWER' => "AAAAAQAAAAEAAAAVAw==", + 'STANDBY' => "AAAAAQAAAAEAAAAvAw==", + 'EXIT' => "AAAAAQAAAAEAAABjAw==", + 'RED' => "AAAAAgAAAJcAAAAlAw==", + 'GREEN' => "AAAAAgAAAJcAAAAmAw==", + 'YELLOW' => "AAAAAgAAAJcAAAAnAw==", + 'BLUE' => "AAAAAgAAAJcAAAAkAw==", + 'HOME' => "AAAAAQAAAAEAAABgAw==", + 'VOLUP' => "AAAAAQAAAAEAAAASAw==", + 'VOLUMEUP' => "AAAAAQAAAAEAAAASAw==", + 'VOLDOWN' => "AAAAAQAAAAEAAAATAw==", + 'VOLUMEDOWN' => "AAAAAQAAAAEAAAATAw==", + 'MUTE' => "AAAAAQAAAAEAAAAUAw==", + 'OPTIONS' => "AAAAAgAAAJcAAAA2Aw==", + 'DOT' => "AAAAAgAAAJcAAAAdAw==", + '0' => "AAAAAQAAAAEAAAAJAw==", + '1' => "AAAAAQAAAAEAAAAAAw==", + '2' => "AAAAAQAAAAEAAAABAw==", + '3' => "AAAAAQAAAAEAAAACAw==", + '4' => "AAAAAQAAAAEAAAADAw==", + '5' => "AAAAAQAAAAEAAAAEAw==", + '6' => "AAAAAQAAAAEAAAAFAw==", + '7' => "AAAAAQAAAAEAAAAGAw==", + '8' => "AAAAAQAAAAEAAAAHAw==", + '9' => "AAAAAQAAAAEAAAAIAw==", + 'GUIDE' => "AAAAAQAAAAEAAAAOAw==", + 'INFO' => "AAAAAQAAAAEAAAA6Aw==", + 'UP' => "AAAAAQAAAAEAAAB0Aw==", + 'DOWN' => "AAAAAQAAAAEAAAB1Aw==", + 'LEFT' => "AAAAAQAAAAEAAAA0Aw==", + 'RIGHT' => "AAAAAQAAAAEAAAAzAw==", + 'OK' => "AAAAAQAAAAEAAABlAw==", + 'RETURN' => "AAAAAgAAAJcAAAAjAw==", + 'NEXT' => "AAAAAgAAAJcAAAA9Aw==", + 'PREVIOUS' => "AAAAAgAAAJcAAAA8Aw==", + 'TV' => "AAAAAgAAABoAAABXAw==", + 'TVPAUSE' => "AAAAAgAAABoAAABnAw==", + 'MODE3D' => "AAAAAgAAAHcAAABNAw==", + 'TEXT' => "AAAAAQAAAAEAAAA/Aw==", + 'SUBTITLE' => "AAAAAgAAAJcAAAAoAw==", + 'CHANUP' => "AAAAAQAAAAEAAAAQAw==", + 'CHANNELUP' => "AAAAAQAAAAEAAAAQAw==", + 'CHANDOWN' => "AAAAAQAAAAEAAAARAw==", + 'CHANNELDOWN' => "AAAAAQAAAAEAAAARAw==", + 'SOURCE' => "AAAAAQAAAAEAAAAlAw==", + 'PLAY' => "AAAAAgAAAJcAAAAaAw==", + 'PAUSE' => "AAAAAgAAAJcAAAAZAw==", + 'FORWARD' => "AAAAAgAAAJcAAAAcAw==", + 'STOP' => "AAAAAgAAAJcAAAAYAw==", + 'REWIND' => "AAAAAgAAAJcAAAAbAw==", + 'RECORD' => "AAAAAgAAAJcAAAAgAw==", + 'ASPECT' => "AAAAAQAAAAEAAAA6Aw==", + 'HELP' => "AAAAAgAAABoAAAB7Aw==", + 'DIGITAL' => "AAAAAgAAABoAAAA7Aw==", + 'TRACKID' => "AAAAAgAAABoAAAB+Aw==", + 'AUDIO' => "AAAAAQAAAAEAAAAXAw==", + 'SEN' => "AAAAAgAAABoAAAB9Aw==", + 'SYNCMENU' => "AAAAAgAAABoAAABYAw==", + 'SCENESELECT' => "AAAAAgAAABoAAAB4Aw==", + }; + + if ( defined( $commands->{$command} ) ) { + return $commands->{$command}; + } + elsif ( $command eq "GetRemotecontrolCommands" ) { + return $commands; + } + else { + return ""; + } +} + +sub BRAVIA_GetModelYear($) { + my ($command) = @_; + my $commands = { + '1.0' => "2011", + '1.1' => "2012", + '1.0.4' => "2013", + '2.4.0' => "2014", + }; + + if (defined( $commands->{$command})) { + return $commands->{$command}; + } else { + return ""; + } +} + +sub BRAVIA_GetIrccRequest($) { + my ($cmd) = @_; + my $data = ""; + $data .= ""; + $data .= ""; + $data .= ""; + $data .= "" . $cmd . ""; + $data .= ""; + $data .= ""; + $data .= ""; + + return $data; +} + +sub BRAVIA_GetUpnpRequest($$) { + my ($cmd,$value) = @_; + my $data = ""; + $data .= ""; + $data .= ""; + if ($cmd eq "getVolume") { + $data .= ""; + $data .= "0"; + $data .= "Master"; + $data .= ""; + } elsif ($cmd eq "setVolume") { + $data .= ""; + $data .= "0"; + $data .= "Master"; + $data .= ""; + $data .= $value; + $data .= ""; + $data .= ""; + } elsif ($cmd eq "getMute") { + $data .= ""; + $data .= "0"; + $data .= "Master"; + $data .= ""; + } elsif ($cmd eq "setMute") { + $data .= ""; + $data .= "0"; + $data .= "Master"; + $data .= ""; + $data .= $value; + $data .= ""; + $data .= ""; + } + $data .= ""; + $data .= ""; + + return $data; +} + +sub BRAVIA_CheckRegistration($) { + my ( $hash ) = @_; + my $name = $hash->{NAME}; + + if (ReadingsVal($name, "authCookie", "") ne "" and + ReadingsTimestamp($name, "authCookie", "") =~ m/^(\d{4})-(\d{2})-(\d{2}) ([0-2]\d):([0-5]\d):([0-5]\d)$/) { + + my $time = fhemTimeLocal($6, $5, $4, $3, $2 - 1, $1 - 1900); + # max age defaults to 14 days + my $maxAge = ReadingsNum($name, "authMaxAge", 1209600); + + # renew registration after half period of validity + if ($time + $maxAge/2 < time()) { + Log3 $name, 3, "BRAVIA $name: renew registration"; + BRAVIA_SendCommand( $hash, "register", "renew" ); + } + } +} + +sub BRAVIA_GetNormalizedName($) { + my ( $name ) = @_; + $name =~ s/^\s+//; + $name =~ s/\s+$//; + $name =~ s/\s/_/g; + $name =~ s/,/./g; + return $name; +} + +1; +=pod +=begin html + + +

BRAVIA

+
    + This module controls a Sony TV device over ethernet. Devices of series starting from 2011 are supported. +

    + Define +
      + define <name> BRAVIA <ip-or-hostname> [<poll-interval>] +

      + With definition of a BRAVIA device an internal task will be scheduled. + This task pulls frequently the status and other information from the TV.
      + The intervall can be defined in seconds by an optional parameter <poll-intervall>. + The default value is 45 seconds. +

      + After definition of a device using this module it has to be registered as a remote control + (set register). +

      + As long as readings are not among the usual AV readings they are clustered: + + + +
      s_*: status
      ci_*: content information
      +

      + The module contains predefined layouts for remotecontrol using PNG and SVG. +

      +
    + + + Set +
      + set <name> <option> <value> +

      + Options: +
        +
      • channel
        + List of all known channels. The module collects all visited channels. + Channels can be loaded automtically with modells from 2013 and newer. + (number of channels, see channelsMax).
      • +
      • channelDown
        + Switches a channel back.
      • +
      • channelUp
        + Switches a channel forward.
      • +
      • mute
        + Set mute if Upnp is activated.
      • +
      • off
        + Switches TV to off.
      • +
      • on
        + Switches TV to on, with modells from 2013 using WOL.
      • +
      • pause
        + Pauses a playing of a recording, of an internal App, etc.
      • +
      • play
        + Starts playing of a recording, of an internal App, etc.
      • +
      • record
        + Starts recording of current content.
      • +
      • register
        + One-time registration of Fhem as remote control in the TV.
        + With requestFormat = "xml" registration works without parameter.
        + With requestFormat = "json" registration has to be executed twice.
        + The register option offers an additional input field: +
          +
        1. Call with empty input. A PIN for registration has to be shown on the TV.
        2. +
        3. Insert PIN into input field and register again.
      • +
      • requestFormat
        + "xml" for xml based communication (modells from 2011 and 2012)
        + "json" for communication with modells from 2013 and newer
      • +
      • remoteControl
        + Sends command directly to TV.
      • +
      • statusRequest
        + Retrieves current status information from TV.
      • +
      • stop
        + Stops recording, playing of an internal App, etc.
      • +
      • toggle
        + Toggles power status of TV.
      • +
      • tvpause
        + Activates Timeshift mode.
      • +
      • upnp
        + Activates Upnp service used to control volume.
      • +
      • volume
        + Straight setting of volume. Upnp service has to be activated.
      • +
      • volumeDown
        + Decreases volume.
      • +
      • volumeUp
        + Increases volume.
      • +
      +
    +
    + + + Attributes +
      + attr <name> <attribute> <value> +

      + Attributes: +
        +
      • channelsMax
        + Maximum amount of channels to be displayed, default is 50.
      • +
      • macaddr
        + Enables power on of TV using WOL.
      • +
      +
    +
+ +=end html +=begin html_DE + + +

BRAVIA

+
    + Diese Module dient zur Steuerung von Sony TVs der BRAVIA-Serien beginnend mit dem Modelljahr 2011. +

    + Define +
      + define <name> BRAVIA <ip-or-hostname> [<poll-interval>] +

      + Bei der Definition eines BRAVIA Gerätes wird ein interner Task eingeplant, + der regelmäßig den Status des TV prüft und weitere Informationen abruft.
      + Das Intervall des Tasks kann durch den optionalen Parameter <poll-intervall> in Sekunden gesetzt werden. + Ansonsten wird der Task mit 45 Sekunden als Intervall definiert. +

      + Nach der Definition eines Gerätes muss dieses einmalig im TV als Fernbedienung + registriert werden (set register). +

      + Soweit die Readings nicht den allgemeinen AV Readings entsprechen, sind sie gruppiert: + + + +
      s_*: Status
      ci_*: Inhaltsinfo
      +

      + Das Modul enthält vorgefertigte Layouts für remotecontrol mit PNG und SVG. +

      +
    + + + Set +
      + set <name> <option> <value> +

      + Optionen: +
        +
      • channel
        + Liste alle bekannten Kanäle. Das Modul merkt sich alle aufgerufenen Kanäle. + Ab Modelljahr 2013 werden die Kanäle automatisch geladen + (Anzahl siehe channelsMax).
      • +
      • channelDown
        + Einen Kanal zurück schalten.
      • +
      • channelUp
        + Einen Kanal weiter schalten.
      • +
      • mute
        + Direkte Stummschaltung erfolgt nur per aktiviertem Upnp.
      • +
      • off
        + Schaltet den TV aus.
      • +
      • on
        + Einschalten des TV, ab Modelljahr 2013 per WOL.
      • +
      • pause
        + Pausiert die Wiedergabe einer Aufnahme, einer internen App, etc.
      • +
      • play
        + Startet die Wiedergabe einer Aufnahme, einer internen App, etc.
      • +
      • record
        + Startet die Aufnahme des aktuellen Inhalts.
      • +
      • register
        + Einmalige Registrierung von FHEM als Fernbedienung im TV.
        + Bei requestFormat = "xml" erfolgt die Registrierung ohne Parameter.
        + Bei requestFormat = "json" ist die Registrierung zweistufig.
        + Beim Aufruf des Setter gibt es ein Eingabefeld: +
          +
        1. Aufruf mit leerem Eingabefeld. Auf dem TV sollte eine PIN zur Registrierung erscheinen.
        2. +
        3. PIN im Eingabefeld eintragen und Registrierung noch mal ausführen
      • +
      • requestFormat
        + "xml" für xml-basierte Kommunikation 2011er/2012er Geräte
        + "json" für die Kommunikation seit der 2013er Generation
      • +
      • remoteControl
        + Direktes Senden von Kommandos an den TV.
      • +
      • statusRequest
        + Ruft die aktuellen Statusinformationen vom TV ab.
      • +
      • stop
        + Stoppt die Wiedergabe einer Aufnahme, einer internen App, etc.
      • +
      • toggle
        + Wechselt den Einschaltstatus des TV.
      • +
      • tvpause
        + Aktiviert den Timeshift-Modus.
      • +
      • upnp
        + Aktiviert Upnp zum Abfragen und Einstellen der Lautstärke.
      • +
      • volume
        + Direktes Setzen der Lautstärke erfolgt nur per aktiviertem Upnp.
      • +
      • volumeDown
        + Verringert die Lautstärke.
      • +
      • volumeUp
        + Erhöht die Lautstärke.
      • +
      +
    +
    + + + Attributes +
      + attr <name> <attribute> <value> +

      + Attribute: +
        +
      • channelsMax
        + Maximale Anzahl der im FHEMWEB angezeigten Kanäle. Der Standartwert ist 50.
      • +
      • macaddr
        + Ermöglicht das Einschalten des TV per WOL.
      • +
      +
    +
+ +=end html_DE +=cut diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt index 2b8d8d98f..a42b0c35e 100644 --- a/fhem/MAINTAINER.txt +++ b/fhem/MAINTAINER.txt @@ -221,6 +221,7 @@ FHEM/63_EMGZ.pm rudolfkoenig http://forum.fhem.de SlowRF FHEM/64_ESA2000.pm stromer-12 http://forum.fhem.de SlowRF FHEM/66_ECMD.pm borisneubert http://forum.fhem.de Sonstige Systeme FHEM/67_ECMDDevice.pm borisneubert http://forum.fhem.de Sonstige Systeme +FHEM/70_BRAVIA.pm vuffiraa http://forum.fhem.de Multimedia FHEM/70_EGPM.pm alexus http://forum.fhem.de Sonstiges FHEM/70_ENIGMA2.pm loredo http://forum.fhem.de Multimedia FHEM/70_Jabber.pm BioS http://forum.fhem.de Unterstuetzende Dienste