# $Id$ 71_DENON_AVR.pm 2016-03-10 07:05:00 xusader $ ############################################################################## # # 71_DENON_AVR.pm # An FHEM Perl module for controlling Denon AV-Receivers # via network connection. # # Currently supported are: power (on|off) # volumeStraight (-80 ... 18) # volume (0 ... 98) # mute (on|off) # input (select input source) # sound (select sound mode) # favorite (1|2|3) # preset (P1|P2|P3) # # In addition, you can send any documented command from the "DENON AVR # protocol documentation" via "rawCommand <command>"; e.g. "rawCommand # PWON" does the exact same thing as "power on" # # Copyright by Boris Pruessmann # # forked by xusader/michaelmueller # forked by quigley # now needs to specify telnetport 23 in define for TCP/IP: # define myDenon DENON_AVR 192.168.0.12:23 # or define for serial port: # define myDenon DENON_AVR /dev/ttyUSB0@9600 # forked by chrpme/MikeUnke # - Favorites can be called now # - Fixed bug while setting volume to values less than 10 # - Code cleanup at SetVolume function # forked by Amenophis86 # - rawCommand mit Leerzeichen # - reconnect / disconnect # - SimpleWrite update # # 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 <http://www.gnu.org/licenses/>. # ############################################################################## package main; use strict; use warnings; use Time::HiRes qw(usleep gettimeofday); ################################### my %commands = ( "power:on" => "PWON", "power:off" => "PWSTANDBY", "mute:on" => "MUON", "mute:off" => "MUOFF" ); my %powerStateTransition = ( "on" => "off", "off" => "on" ); my %inputs = ( "TUNER" => "", "DVD" => "", "BD" => "", "TV" => "", "SAT/CBL" => "", "MPLAY" => "", "GAME" => "", "AUX1" => "", "NET" => "", "SPOTIFY" => "", "FLICKR" => "", "FAVORITES" => "", "IRADIO" => "", "SERVER" => "", "USB/IPOD" => "", "USB" => "", "IPD" => "", "IRP" => "", "FVP" => "" ); my %sounds = ( "MOVIE" => "", "MUSIC" => "", "GAME" => "", "DIRECT" => "", "STEREO" => "", "STANDARD" => "", "DOLBY_DIGITAL" => "", "DTS_SURROUND" => "", "MCH_STEREO" => "", "ROCK_ARENA" => "", "JAZZ_CLUB" => "", "MONO_MOVIE" => "", "MATRIX" => "", "VIDEO_GAME" => "", "VIRTUAL" => "" ); ################################### sub DENON_AVR_Initialize($) { my ($hash) = @_; Log 5, "DENON_AVR_Initialize: Entering"; require "$attr{global}{modpath}/FHEM/DevIo.pm"; # Provider $hash->{ReadFn} = "DENON_AVR_Read"; $hash->{WriteFn} = "DENON_AVR_Write"; # Device $hash->{DefFn} = "DENON_AVR_Define"; $hash->{UndefFn} = "DENON_AVR_Undefine"; $hash->{GetFn} = "DENON_AVR_Get"; $hash->{SetFn} = "DENON_AVR_Set"; $hash->{AttrFn} = "DENON_AVR_Attr"; $hash->{ShutdownFn} = "DENON_AVR_Shutdown"; $hash->{AttrList} = "do_not_notify:0,1 loglevel:0,1,2,3,4,5 do_not_send_commands:0,1 keepalive ".$readingFnAttributes; } ##################################### sub DENON_AVR_DoInit($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_DoInit: Called for $name"; DENON_AVR_Command_StatusRequest($hash); $hash->{STATE} = "Initialized"; return undef; } ################################### sub DENON_AVR_Read($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Read: Called for $name"; my $buf = DevIo_SimpleRead($hash); return "" if (!defined($buf)); my $culdata = $hash->{PARTIAL}; Log $ll5, "DENON_AVR_Read: $culdata/$buf"; $culdata .= $buf; readingsBeginUpdate($hash); while ($culdata =~ m/\r/) { my $rmsg; ($rmsg, $culdata) = split("\r", $culdata, 2); $rmsg =~ s/\r//; DENON_AVR_Parse($hash, $rmsg) if($rmsg); } readingsEndUpdate($hash, 1); $hash->{PARTIAL} = $culdata; } ##################################### sub DENON_AVR_Write($$$) { my ($hash, $fn, $msg) = @_; Log 5, "DENON_AVR_Write: Called"; } ################################### ################################### sub DENON_AVR_SimpleWrite(@) { my ($hash, $msg) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_SimpleWrite: $msg"; my $doNotSendCommands = AttrVal($name, "do_not_send_commands", "0"); if ($doNotSendCommands ne "1") { #syswrite($hash->{TCPDev}, $msg."\r") if ($hash->{TCPDev}); #$hash->{USBDev}->write($msg."\r") if($hash->{USBDev}); DevIo_SimpleWrite($hash, $msg."\r", 0); # Let's wait 100ms - not sure if still needed usleep(100 * 1000); # Some linux installations are broken with 0.001, T01 returns no answer select(undef, undef, undef, 0.01); } } ################################### sub DENON_AVR_Parse(@) { my ($hash, $msg) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Parse: Parsing <$msg>"; if ($msg =~ /PW(.+)/) { my $power = lc($1); if ($power eq "standby") { $power = "off"; } readingsBulkUpdate($hash, "power", $power); $hash->{STATE} = $power; } elsif ($msg =~ /MU(.+)/) { readingsBulkUpdate($hash, "mute", lc($1)); } elsif ($msg =~ /MVMAX (.+)/) { Log $ll5, "DENON_AVR_Parse: Ignoring maximum volume of <$1>"; } elsif ($msg =~ /MV(.+)/) { my $volume = $1; if (length($volume) == 2) { $volume = $volume."0"; } readingsBulkUpdate($hash, "volumeStraight", $volume / 10 - 80); readingsBulkUpdate($hash, "volume", $volume / 10); } elsif ($msg =~/SI(.+)/) { readingsBulkUpdate($hash, "input", $1); } elsif ($msg =~/MS(.+)/) { readingsBulkUpdate($hash, "sound", $1); } else { Log $ll5, "DENON_AVR_Parse: Unknown message <$msg>"; } } ################################### sub DENON_AVR_Define($$) { my ($hash, $def) = @_; Log 5, "DENON_AVR_Define($def) called."; my @a = split("[ \t][ \t]*", $def); if (@a != 3) { my $msg = "wrong syntax: define <name> DENON_AVR <ip-or-hostname>"; Log 2, $msg; return $msg; } DevIo_CloseDev($hash); my $name = $a[0]; my $host = $a[2]; #$hash->{DeviceName} = $host.":23"; $hash->{DeviceName} = $host; my $ret = DevIo_OpenDev($hash, 0, "DENON_AVR_DoInit"); InternalTimer(gettimeofday() + 5, "DENON_AVR_UpdateConfig", $hash, 0); unless (exists($attr{$name}{webCmd})){ $attr{$name}{webCmd} = 'volumeStraight:mute:input:sound:favorite:preset'; } unless (exists($attr{$name}{stateFormat})){ $attr{$name}{stateFormat} = 'power'; } return $ret; } ############################# sub DENON_AVR_Undefine($$) { my($hash, $name) = @_; Log 5, "DENON_AVR_Undefine: Called for $name"; RemoveInternalTimer($hash); DevIo_CloseDev($hash); return undef; } ############################# sub DENON_AVR_Get($@) { my ($hash, @a) = @_; my $what; return "argument is missing" if (int(@a) != 2); $what = $a[1]; if ($what =~ /^(power|volumeStraight|volume|volumeDown|volumeUp|mute|input|sound)$/) { if(defined($hash->{READINGS}{$what})) { return $hash->{READINGS}{$what}{VAL}; } else { return "no such reading: $what"; } } else { return "Unknown argument $what, choose one of power volumeStraight volume volumeDown volumeUp mute input sound"; } } ################################### sub DENON_AVR_Set($@) { my ($hash, @a) = @_; my $what = $a[1]; my $usage = "Unknown argument $what, choose one of favorite:1,2,3 preset:P1,P2,P3 on off toggle volumeDown volumeUp volumeStraight:slider,-80,1,18 volume:slider,0,1,98 mute:on,off reconnect disconnect " . "input:" . join(",", sort keys %inputs) . " " . "sound:" . join(",", sort keys %sounds) . " " . "rawCommand statusRequest"; if ($what =~ /^(on|off)$/) { return DENON_AVR_Command_SetPower($hash, $what); } elsif ($what eq "favorite") { my $favorite = $a[2]; return DENON_AVR_Command_SetFavorite($hash, $favorite); } elsif ($what eq "preset") { my $preset = $a[2]; return DENON_AVR_Command_SetPreset($hash, $preset); } elsif ($what eq "toggle") { my $newPowerState = $powerStateTransition{$hash->{STATE}}; return $newPowerState if (!defined($newPowerState)); return DENON_AVR_Command_SetPower($hash, $newPowerState); } elsif ($what eq "mute") { my $mute = $a[2]; return DENON_AVR_Command_SetMute($hash, $mute); } elsif ($what eq "input") { my $input = $a[2]; return DENON_AVR_Command_SetInput($hash, $input); } elsif ($what eq "sound") { my $sound = $a[2]; if ( $sound eq "DOLBY_DIGITAL") { $sound = "DOLBY DIGITAL"; } elsif ($sound eq "DTS_SURROUND") { $sound = "DTS SURROUND"; } elsif ($sound eq "MCH_STEREO") { $sound = "MCH STEREO"; } elsif ($sound eq "ROCK_ARENA") { $sound = "ROCK ARENA"; } elsif ($sound eq "JAZZ_CLUB") { $sound = "JAZZ CLUB"; } elsif ($sound eq "MONO_MOVIE") { $sound = "MONO MOVIE"; } elsif ($sound eq "VIDEO_GAME") { $sound = "VIDEO GAME"; } return DENON_AVR_Command_SetSound($hash, $sound); } elsif ($what eq "volumeStraight") { my $volume = $a[2]; return DENON_AVR_Command_SetVolume($hash, $volume + 80); } elsif ($what eq "volume") { my $volume = $a[2]; return DENON_AVR_Command_SetVolume($hash, $volume); } elsif ($what eq "volumeDown") { my $cmd = "MVDOWN"; DENON_AVR_SimpleWrite($hash, $cmd); } elsif ($what eq "volumeUp") { my $cmd = "MVUP"; DENON_AVR_SimpleWrite($hash, $cmd); } elsif ($what eq "rawCommand") { my $cmd = $a[2]; $cmd = $a[2]." ".$a[3] if defined $a[3]; DENON_AVR_SimpleWrite($hash, $cmd); } elsif ($what eq "statusRequest") { # Force update of status return DENON_AVR_Command_StatusRequest($hash); } elsif ($what eq "reconnect") { my $status = $hash->{READINGS}{"state"}{VAL}; if($status ne "opened") { DevIo_OpenDev($hash, 0, "DENON_AVR_DoInit"); } else { return "Device must be connected to disconnect!"; } } elsif ($what eq "disconnect") { my $name = $hash->{NAME}; DevIo_CloseDev($hash); $hash->{STATE} = "disconnected"; setReadingsVal($hash, "state", "disconnected", TimeNow()); Log 1, "$name: closed!"; } else { return $usage; } return undef; } ################################### sub DENON_AVR_Attr($@) { my @a = @_; my $what = $a[2]; if ($what eq "keepalive") { my $name = $a[1]; my $hash = $defs{$name}; my $keepalive = $a[3]; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Attr: Changing keepalive to <$keepalive> seconds"; RemoveInternalTimer($hash); InternalTimer(gettimeofday() + $keepalive, "DENON_AVR_KeepAlive", $hash, 0); } return undef; } ##################################### sub DENON_AVR_Shutdown($) { my ($hash) = @_; Log 5, "DENON_AVR_Shutdown: Called"; } ##################################### sub DENON_AVR_UpdateConfig($) { # this routine is called 5 sec after the last define of a restart # this gives FHEM sufficient time to fill in attributes # it will also be called after each manual definition # Purpose is to parse attributes and read config my ($hash) = @_; my $name = $hash->{NAME}; my $webCmd = AttrVal($name, "webCmd", ""); if (!$webCmd) { $attr{$name}{webCmd} = "volumeStraight:mute:input:sound"; } my $keepalive = AttrVal($name, "keepalive", 5 * 60); RemoveInternalTimer($hash); InternalTimer(gettimeofday() + $keepalive, "DENON_AVR_KeepAlive", $hash, 0); } ##################################### sub DENON_AVR_KeepAlive($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_KeepAlive: Called for $name"; DENON_AVR_SimpleWrite($hash, "PW?"); my $keepalive = AttrVal($name, "keepalive", 5 * 60); RemoveInternalTimer($hash); InternalTimer(gettimeofday() + $keepalive, "DENON_AVR_KeepAlive", $hash, 0); } ##################################### sub DENON_AVR_Command_SetPower($$) { my ($hash, $power) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetPower: Called for $name"; my $command = $commands{"power:".lc($power)}; DENON_AVR_SimpleWrite($hash, $command); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "power", $power); readingsEndUpdate($hash, 1); return undef; } ##################################### sub DENON_AVR_Command_SetMute($$) { my ($hash, $mute) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetMute: Called for $name"; return "mute can only used when device is powered on" if ($hash->{STATE} eq "off"); my $command = $commands{"mute:".lc($mute)}; DENON_AVR_SimpleWrite($hash, $command); return undef; } ##################################### sub DENON_AVR_Command_SetInput($$) { my ($hash, $input) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetInput: Called for $name"; DENON_AVR_SimpleWrite($hash, "SI".$input); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "input", $input); readingsEndUpdate($hash, 1); return undef; } ##################################### sub DENON_AVR_Command_SetSound($$) { my ($hash, $sound) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetSound: Called for $name"; DENON_AVR_SimpleWrite($hash, "MS".$sound); readingsBeginUpdate($hash); readingsBulkUpdate($hash, "sound", $sound); readingsEndUpdate($hash, 1); return undef; } ##################################### sub DENON_AVR_Command_SetFavorite($$) { my ($hash, $favorite) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetFavorite: Called for $name"; DENON_AVR_SimpleWrite($hash, "ZMFAVORITE".$favorite); return undef; } ##################################### sub DENON_AVR_Command_SetPreset($$) { my ($hash, $preset) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetPreset: Called for $name"; DENON_AVR_SimpleWrite($hash, "NS".$preset); return undef; } ##################################### sub DENON_AVR_Command_SetVolume($$) { my ($hash, $volume) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_SetVolume: Called for $name"; if($hash->{STATE} eq "off") { return "volume can only used when device is powered on"; } else { if ($volume % 1 == 0) { $volume = sprintf ('%03d', ($volume * 10)); } else { $volume = sprintf ('%02d', $volume); } DENON_AVR_SimpleWrite($hash, "MV".$volume); } return undef; } ##################################### sub DENON_AVR_Command_StatusRequest($) { my ($hash) = @_; my $name = $hash->{NAME}; my $ll5 = GetLogLevel($name, 5); Log $ll5, "DENON_AVR_Command_StatusRequest: Called for $name"; DENON_AVR_SimpleWrite($hash, "PW?"); DENON_AVR_SimpleWrite($hash, "MU?"); DENON_AVR_SimpleWrite($hash, "MV?"); DENON_AVR_SimpleWrite($hash, "SI?"); DENON_AVR_SimpleWrite($hash, "MS?"); DENON_AVR_SimpleWrite($hash, "NSP"); DENON_AVR_SimpleWrite($hash, "ZM?"); DENON_AVR_SimpleWrite($hash, "Z2?"); DENON_AVR_SimpleWrite($hash, "Z3?"); DENON_AVR_SimpleWrite($hash, "SLP?"); return undef; } 1;