diff --git a/fhem/MAINTAINER.txt b/fhem/MAINTAINER.txt
index 1f7bfd1b1..a1d467930 100644
--- a/fhem/MAINTAINER.txt
+++ b/fhem/MAINTAINER.txt
@@ -401,6 +401,7 @@ FHEM/GPUtils.pm ntruchsess http://forum.fhem.de FHEM Deve
contrib/23_WEBTHERM.pm betateilchen/sachag http://forum.fhem.de Sonstiges
contrib/55_BBB_BMP180.pm betateilchen http://forum.fhem.de Einplatinencomputer
contrib/55_weco.pm betateilchen http://forum.fhem.de Wettermodule
+contrib/70_ONKYO_AVR_PULL.pm loredo http://forum.fhem.de Multimedia
contrib/71_LISTENLIVE.pm betateilchen http://forum.fhem.de Multimedia
contrib/98_geodata.pm betateilchen http://forum.fhem.de Sonstiges
contrib/98_openweathermap.pm betateilchen http://forum.fhem.de Wettermodule
diff --git a/fhem/contrib/70_ONKYO_AVR_PULL.pm b/fhem/contrib/70_ONKYO_AVR_PULL.pm
new file mode 100644
index 000000000..a290eb06b
--- /dev/null
+++ b/fhem/contrib/70_ONKYO_AVR_PULL.pm
@@ -0,0 +1,1826 @@
+# $Id$
+##############################################################################
+#
+# 70_ONKYO_AVR_PULL.pm
+# An FHEM Perl module for controlling ONKYO A/V receivers
+# via network connection using a cyclic pull mechanism.
+#
+# +++ THIS VERSION IS _NOT_ UNDER ACTIVE DEVELOPMENT +++
+# +++ ============================================== +++
+# +++ For the benefit to get rid of pulling and getting +++
+# +++ real-time updates, use the official version +++
+# +++ ../FHEM/70_ONKYO_AVR.pm instead. +++
+#
+# Copyright by Julian Pawlowski
+# e-mail: julian.pawlowski 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 strict;
+use warnings;
+use ONKYOdb;
+use IO::Socket;
+use IO::Handle;
+use IO::Select;
+use XML::Simple;
+use Time::HiRes qw(usleep);
+use Symbol qw;
+use Data::Dumper;
+
+no if $] >= 5.017011, warnings => 'experimental::smartmatch';
+
+sub ONKYO_AVR_PULL_Set($@);
+sub ONKYO_AVR_PULL_Get($@);
+sub ONKYO_AVR_PULL_GetStatus($;$);
+sub ONKYO_AVR_PULL_Define($$);
+sub ONKYO_AVR_PULL_Undefine($$);
+
+#########################
+# Forward declaration for remotecontrol module
+sub ONKYO_AVR_PULL_RClayout_TV();
+sub ONKYO_AVR_PULL_RCmakenotify($$);
+
+###################################
+sub ONKYO_AVR_PULL_Initialize($) {
+ my ($hash) = @_;
+
+ Log3 $hash, 5, "ONKYO_AVR_PULL_Initialize: Entering";
+
+ $hash->{GetFn} = "ONKYO_AVR_PULL_Get";
+ $hash->{SetFn} = "ONKYO_AVR_PULL_Set";
+ $hash->{DefFn} = "ONKYO_AVR_PULL_Define";
+ $hash->{UndefFn} = "ONKYO_AVR_PULL_Undefine";
+
+ $hash->{AttrList} =
+"volumeSteps:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 inputs disable:0,1 model wakeupCmd:textField "
+ . $readingFnAttributes;
+
+ # $data{RC_layout}{ONKYO_AVR_PULL_SVG} = "ONKYO_AVR_PULL_RClayout_SVG";
+ # $data{RC_layout}{ONKYO_AVR_PULL} = "ONKYO_AVR_PULL_RClayout";
+ $data{RC_makenotify}{ONKYO_AVR_PULL} = "ONKYO_AVR_PULL_RCmakenotify";
+}
+
+#####################################
+sub ONKYO_AVR_PULL_GetStatus($;$) {
+ my ( $hash, $local ) = @_;
+ my $name = $hash->{NAME};
+ my $interval = $hash->{INTERVAL};
+ my $zone = $hash->{ZONE};
+ my $protocol = $hash->{READINGS}{deviceyear}{VAL};
+ my $state = '';
+ my $reading;
+ my $states;
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_GetStatus()";
+
+ $local = 0 unless ( defined($local) );
+ if ( defined( $attr{$name}{disable} ) && $attr{$name}{disable} eq "1" ) {
+ return $hash->{STATE};
+ }
+
+ InternalTimer( gettimeofday() + $interval, "ONKYO_AVR_PULL_GetStatus", $hash, 1 )
+ unless ( $local == 1 );
+
+ # cache XML device information
+ #
+ # get device information if not available from helper
+ if ( !defined( $hash->{helper}{receiver} )
+ && $protocol ne "pre2013"
+ && $hash->{READINGS}{presence}{VAL} ne "absent" )
+ {
+ my $xml =
+ ONKYO_AVR_PULL_SendCommand( $hash, "net-receiver-information", "query" );
+
+ if ( defined($xml) && $xml =~ /^<\?xml/ ) {
+
+ my $xml_parser = XML::Simple->new(
+ NormaliseSpace => 2,
+ KeepRoot => 0,
+ ForceArray => 0,
+ SuppressEmpty => 1
+ );
+ $hash->{helper}{receiver} = $xml_parser->XMLin($xml);
+
+ # Safe input names
+ my $inputs;
+ foreach my $input (
+ sort
+ keys
+ %{ $hash->{helper}{receiver}{device}{selectorlist}{selector} }
+ )
+ {
+ if ( $input ne "" ) {
+ my $id =
+ uc( $hash->{helper}{receiver}{device}{selectorlist}
+ {selector}{$input}{id} );
+ $input =~ s/\s/_/g;
+ $hash->{helper}{receiver}{input}{$id} = $input;
+ $inputs .= $input . ":";
+ }
+ }
+ if ( !defined( $attr{$name}{inputs} ) ) {
+ $inputs = substr( $inputs, 0, -1 );
+ $attr{$name}{inputs} = $inputs;
+ }
+
+ readingsBeginUpdate($hash);
+
+ # Brand
+ $reading = "brand";
+ if (
+ defined( $hash->{helper}{receiver}{device}{$reading} )
+ && ( !defined( $hash->{READINGS}{$reading}{VAL} )
+ || $hash->{READINGS}{$reading}{VAL} ne
+ $hash->{helper}{receiver}{device}{$reading} )
+ )
+ {
+ readingsBulkUpdate( $hash, $reading,
+ $hash->{helper}{receiver}{device}{$reading} );
+ }
+
+ # Model
+ $reading = "model";
+ if (
+ defined( $hash->{helper}{receiver}{device}{$reading} )
+ && ( !defined( $hash->{READINGS}{$reading}{VAL} )
+ || $hash->{READINGS}{$reading}{VAL} ne
+ $hash->{helper}{receiver}{device}{$reading} )
+ )
+ {
+ readingsBulkUpdate( $hash, $reading,
+ $hash->{helper}{receiver}{device}{$reading} );
+
+ if ( !exists( $attr{$name}{model} )
+ || $attr{$name}{model} ne
+ $hash->{helper}{receiver}{device}{$reading} )
+ {
+ $attr{$name}{model} =
+ $hash->{helper}{receiver}{device}{$reading};
+ }
+ }
+
+ # Firmware version
+ $reading = "firmwareversion";
+ if (
+ defined( $hash->{helper}{receiver}{device}{$reading} )
+ && ( !defined( $hash->{READINGS}{$reading}{VAL} )
+ || $hash->{READINGS}{$reading}{VAL} ne
+ $hash->{helper}{receiver}{device}{$reading} )
+ )
+ {
+ readingsBulkUpdate( $hash, $reading,
+ $hash->{helper}{receiver}{device}{$reading} );
+ }
+
+ # device_id
+ $reading = "deviceid";
+ if (
+ defined( $hash->{helper}{receiver}{device}{id} )
+ && ( !defined( $hash->{READINGS}{$reading}{VAL} )
+ || $hash->{READINGS}{$reading}{VAL} ne
+ $hash->{helper}{receiver}{device}{id} )
+ )
+ {
+ readingsBulkUpdate( $hash, $reading,
+ $hash->{helper}{receiver}{device}{id} );
+ }
+
+ # device_year
+ $reading = "deviceyear";
+ if (
+ defined( $hash->{helper}{receiver}{device}{year} )
+ && ( !defined( $hash->{READINGS}{$reading}{VAL} )
+ || $hash->{READINGS}{$reading}{VAL} ne
+ $hash->{helper}{receiver}{device}{year} )
+ )
+ {
+ readingsBulkUpdate( $hash, $reading,
+ $hash->{helper}{receiver}{device}{year} );
+ }
+
+ readingsEndUpdate( $hash, 1 );
+ }
+ elsif ( $hash->{READINGS}{presence}{VAL} ne "absent" ) {
+ Log3 $name, 3,
+"ONKYO_AVR_PULL $name: net-receiver-information command unsupported, this must be a pre2013 device! Implicit fallback to protocol version pre2013.";
+ readingsBeginUpdate($hash);
+ readingsBulkUpdate( $hash, "deviceyear", "pre2013" );
+ readingsEndUpdate( $hash, 1 );
+ unless ( exists( $attr{$name}{model} ) ) {
+ $attr{$name}{model} = "pre2013";
+ }
+ }
+
+ # Input alias handling
+ #
+ if ( defined( $attr{$name}{inputs} ) ) {
+ my @inputs = split( ':', $attr{$name}{inputs} );
+
+ if (@inputs) {
+ foreach (@inputs) {
+ if (m/[^,\s]+(,[^,\s]+)+/) {
+ my @input_names = split( ',', $_ );
+ $input_names[1] =~ s/\s/_/g;
+ $hash->{helper}{receiver}{input_aliases}
+ { $input_names[0] } = $input_names[1];
+ $hash->{helper}{receiver}{input_names}
+ { $input_names[1] } = $input_names[0];
+ }
+ }
+ }
+ }
+ }
+
+ # Read powerstate
+ #
+ my $powerstate = ONKYO_AVR_PULL_SendCommand( $hash, "power", "query" );
+
+ $state = "off";
+ if ( defined($powerstate) ) {
+ if ( $powerstate eq "on" ) {
+ $state = "on";
+
+ # Read other state information
+ $states->{mute} = ONKYO_AVR_PULL_SendCommand( $hash, "mute", "query" );
+ $states->{volume} =
+ ONKYO_AVR_PULL_SendCommand( $hash, "volume", "query" );
+ $states->{sleep} = ONKYO_AVR_PULL_SendCommand( $hash, "sleep", "query" )
+ if ( $zone eq "main" );
+ $states->{input} = ONKYO_AVR_PULL_SendCommand( $hash, "input", "query" );
+ $states->{video} =
+ ONKYO_AVR_PULL_SendCommand( $hash, "video-information", "query" )
+ if ( $zone eq "main" );
+ $states->{audio} =
+ ONKYO_AVR_PULL_SendCommand( $hash, "audio-information", "query" )
+ if ( $zone eq "main" );
+ }
+ }
+ else {
+ $state = "absent";
+ }
+
+ readingsBeginUpdate($hash);
+
+ # Set reading for power
+ #
+ my $readingPower = "off";
+ if ( $state eq "on" ) {
+ $readingPower = "on";
+ }
+
+ if ( !defined( $hash->{READINGS}{power}{VAL} )
+ || $hash->{READINGS}{power}{VAL} ne $readingPower )
+ {
+ readingsBulkUpdate( $hash, "power", $readingPower );
+ }
+
+ # Set reading for state
+ #
+ if ( !defined( $hash->{READINGS}{state}{VAL} )
+ || $hash->{READINGS}{state}{VAL} ne $state )
+ {
+ readingsBulkUpdate( $hash, "state", $state );
+ }
+
+ # Set general readings for all zones
+ #
+ foreach ( "mute", "volume", "input" ) {
+ if ( defined( $states->{$_} ) && $states->{$_} ne "" ) {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne $states->{$_} )
+ {
+ readingsBulkUpdate( $hash, $_, $states->{$_} );
+ }
+ }
+ else {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+
+ # Process for main zone only
+ #
+ if ( $zone eq "main" ) {
+
+ # Set reading for sleep
+ #
+ foreach ("sleep") {
+ if ( defined( $states->{$_} ) && $states->{$_} ne "" ) {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne $states->{$_} )
+ {
+ readingsBulkUpdate( $hash, $_, $states->{$_} );
+ }
+ }
+ else {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+
+ # Set readings for audio
+ #
+ if ( defined( $states->{audio} ) ) {
+ my @audio_split = split( /,/, $states->{audio} );
+ if ( scalar(@audio_split) >= 6 ) {
+
+ # Audio-in sampling rate
+ my ($audin_srate) = split /[:\s]+/, $audio_split[2], 2;
+
+ # Audio-in channels
+ my ($audin_ch) = split /[:\s]+/, $audio_split[3], 2;
+
+ # Audio-out channels
+ my ($audout_ch) = split /[:\s]+/, $audio_split[5], 2;
+
+ if ( !defined( $hash->{READINGS}{audin_src}{VAL} )
+ || $hash->{READINGS}{audin_src}{VAL} ne $audio_split[0] )
+ {
+ readingsBulkUpdate( $hash, "audin_src", $audio_split[0] );
+ }
+ if ( !defined( $hash->{READINGS}{audin_enc}{VAL} )
+ || $hash->{READINGS}{audin_enc}{VAL} ne $audio_split[1] )
+ {
+ readingsBulkUpdate( $hash, "audin_enc", $audio_split[1] );
+ }
+ if (
+ !defined( $hash->{READINGS}{audin_srate}{VAL} )
+ || ( defined($audin_srate)
+ && $hash->{READINGS}{audin_srate}{VAL} ne $audin_srate )
+ )
+ {
+ readingsBulkUpdate( $hash, "audin_srate", $audin_srate );
+ }
+ if (
+ !defined( $hash->{READINGS}{audin_ch}{VAL} )
+ || ( defined($audin_ch)
+ && $hash->{READINGS}{audin_ch}{VAL} ne $audin_ch )
+ )
+ {
+ readingsBulkUpdate( $hash, "audin_ch", $audin_ch );
+ }
+ if ( !defined( $hash->{READINGS}{audout_mode}{VAL} )
+ || $hash->{READINGS}{audout_mode}{VAL} ne $audio_split[4] )
+ {
+ readingsBulkUpdate( $hash, "audout_mode", $audio_split[4] );
+ }
+ if (
+ !defined( $hash->{READINGS}{audout_ch}{VAL} )
+ || ( defined($audout_ch)
+ && $hash->{READINGS}{audout_ch}{VAL} ne $audout_ch )
+ )
+ {
+ readingsBulkUpdate( $hash, "audout_ch", $audout_ch );
+ }
+ }
+ else {
+ foreach (
+ "audin_src", "audin_enc", "audin_srate",
+ "audin_ch", "audout_ch", "audout_mode",
+ )
+ {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+ }
+ else {
+ foreach (
+ "audin_src", "audin_enc", "audin_srate",
+ "audin_ch", "audout_ch", "audout_mode",
+ )
+ {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+
+ # Set readings for video
+ #
+ if ( defined( $states->{video} ) ) {
+ my @video_split = split( /,/, $states->{video} );
+ if ( scalar(@video_split) >= 9 ) {
+
+ # Video-in resolution
+ my @vidin_res_string = split( / +/, $video_split[1] );
+ my $vidin_res;
+ if ( defined( $vidin_res_string[0] )
+ && defined( $vidin_res_string[2] )
+ && defined( $vidin_res_string[3] )
+ && uc( $vidin_res_string[0] ) ne "UNKNOWN"
+ && uc( $vidin_res_string[2] ) ne "UNKNOWN"
+ && uc( $vidin_res_string[3] ) ne "UNKNOWN" )
+ {
+ $vidin_res =
+ $vidin_res_string[0] . "x"
+ . $vidin_res_string[2]
+ . $vidin_res_string[3];
+ }
+ else {
+ $vidin_res = "";
+ }
+
+ # Video-out resolution
+ my @vidout_res_string = split( / +/, $video_split[5] );
+ my $vidout_res;
+ if ( defined( $vidout_res_string[0] )
+ && defined( $vidout_res_string[2] )
+ && defined( $vidout_res_string[3] )
+ && uc( $vidout_res_string[0] ) ne "UNKNOWN"
+ && uc( $vidout_res_string[2] ) ne "UNKNOWN"
+ && uc( $vidout_res_string[3] ) ne "UNKNOWN" )
+ {
+ $vidout_res =
+ $vidout_res_string[0] . "x"
+ . $vidout_res_string[2]
+ . $vidout_res_string[3];
+ }
+ else {
+ $vidout_res = "";
+ }
+
+ # Video-in color depth
+ my ($vidin_cdepth) =
+ split( /[:\s]+/, $video_split[3], 2 ) || "";
+
+ # Video-out color depth
+ my ($vidout_cdepth) =
+ split( /[:\s]+/, $video_split[7], 2 ) || "";
+
+ if ( !defined( $hash->{READINGS}{vidin_src}{VAL} )
+ || $hash->{READINGS}{vidin_src}{VAL} ne $video_split[0] )
+ {
+ readingsBulkUpdate( $hash, "vidin_src", $video_split[0] );
+ }
+ if ( !defined( $hash->{READINGS}{vidin_res}{VAL} )
+ || $hash->{READINGS}{vidin_res}{VAL} ne $vidin_res )
+ {
+ readingsBulkUpdate( $hash, "vidin_res", $vidin_res );
+ }
+ if ( !defined( $hash->{READINGS}{vidin_cspace}{VAL} )
+ || $hash->{READINGS}{vidin_cspace}{VAL} ne
+ lc( $video_split[2] ) )
+ {
+ readingsBulkUpdate( $hash, "vidin_cspace",
+ lc( $video_split[2] ) );
+ }
+ if ( !defined( $hash->{READINGS}{vidin_cdepth}{VAL} )
+ || $hash->{READINGS}{vidin_cdepth}{VAL} ne $vidin_cdepth )
+ {
+ readingsBulkUpdate( $hash, "vidin_cdepth", $vidin_cdepth );
+ }
+ if ( !defined( $hash->{READINGS}{vidout_dst}{VAL} )
+ || $hash->{READINGS}{vidout_dst}{VAL} ne $video_split[4] )
+ {
+ readingsBulkUpdate( $hash, "vidout_dst", $video_split[4] );
+ }
+ if ( !defined( $hash->{READINGS}{vidout_res}{VAL} )
+ || $hash->{READINGS}{vidout_res}{VAL} ne $vidout_res )
+ {
+ readingsBulkUpdate( $hash, "vidout_res", $vidout_res );
+ }
+ if ( !defined( $hash->{READINGS}{vidout_cspace}{VAL} )
+ || $hash->{READINGS}{vidout_cspace}{VAL} ne
+ lc( $video_split[6] ) )
+ {
+ readingsBulkUpdate( $hash, "vidout_cspace",
+ lc( $video_split[6] ) );
+ }
+ if ( !defined( $hash->{READINGS}{vidout_cdepth}{VAL} )
+ || $hash->{READINGS}{vidout_cdepth}{VAL} ne $vidout_cdepth )
+ {
+ readingsBulkUpdate( $hash, "vidout_cdepth",
+ $vidout_cdepth );
+ }
+ if ( !defined( $hash->{READINGS}{vidout_mode}{VAL} )
+ || $hash->{READINGS}{vidout_mode}{VAL} ne
+ lc( $video_split[8] ) )
+ {
+ readingsBulkUpdate( $hash, "vidout_mode",
+ lc( $video_split[8] ) );
+ }
+ }
+ else {
+ foreach (
+ "vidin_src", "vidin_res", "vidin_cspace",
+ "vidin_cdepth", "vidout_dst", "vidout_res",
+ "vidout_cspace", "vidout_cdepth", "vidout_mode",
+ )
+ {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+ }
+ else {
+ foreach (
+ "vidin_src", "vidin_res", "vidin_cspace",
+ "vidin_cdepth", "vidout_dst", "vidout_res",
+ "vidout_cspace", "vidout_cdepth", "vidout_mode",
+ )
+ {
+ if ( !defined( $hash->{READINGS}{$_}{VAL} )
+ || $hash->{READINGS}{$_}{VAL} ne "-" )
+ {
+ readingsBulkUpdate( $hash, $_, "-" );
+ }
+ }
+ }
+ }
+
+ readingsEndUpdate( $hash, 1 );
+
+ Log3 $name, 4, "ONKYO_AVR_PULL $name: " . $hash->{STATE};
+
+ return $hash->{STATE};
+}
+
+###################################
+sub ONKYO_AVR_PULL_Get($@) {
+ my ( $hash, @a ) = @_;
+ my $name = $hash->{NAME};
+ my $what;
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_Get()";
+
+ return "argument is missing" if ( int(@a) < 2 );
+
+ $what = $a[1];
+
+ if ( $what =~ /^(power|input|volume|mute|sleep)$/ ) {
+ 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:noArg input:noArg volume:noArg mute:noArg sleep:noArg ";
+ }
+}
+
+###################################
+sub ONKYO_AVR_PULL_Set($@) {
+ my ( $hash, @a ) = @_;
+ my $name = $hash->{NAME};
+ my $interval = $hash->{INTERVAL};
+ my $zone = $hash->{ZONE};
+ my $state = $hash->{STATE};
+ my $return;
+ my $reading;
+ my $inputs_txt = "";
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_Set()";
+
+ return "No argument given to ONKYO_AVR_PULL_Set" if ( !defined( $a[1] ) );
+
+ # depending on current FHEMWEB instance's allowedCommands,
+ # restrict set commands if there is "set-user" in it
+ my $adminMode = 1;
+ my $FWallowedCommands = 0;
+ $FWallowedCommands = AttrVal( $FW_wname, "allowedCommands", 0 )
+ if ( defined($FW_wname) );
+ if ( $FWallowedCommands && $FWallowedCommands =~ m/\bset-user\b/ ) {
+ $adminMode = 0;
+ return "Forbidden command: set " . $a[1]
+ if ( lc( $a[1] ) eq "statusrequest"
+ || lc( $a[1] ) eq "remotecontrol" );
+ }
+
+ # Input alias handling
+ if ( defined( $attr{$name}{inputs} ) && $attr{$name}{inputs} ne "" ) {
+ my @inputs = split( ':', $attr{$name}{inputs} );
+ $inputs_txt = "-," if ( $state ne "on" );
+
+ if (@inputs) {
+ foreach (@inputs) {
+ if (m/[^,\s]+(,[^,\s]+)+/) {
+ my @input_names = split( ',', $_ );
+ $inputs_txt .= $input_names[1] . ",";
+ $input_names[1] =~ s/\s/_/g;
+ $hash->{helper}{receiver}{input_aliases}{ $input_names[0] }
+ = $input_names[1];
+ $hash->{helper}{receiver}{input_names}{ $input_names[1] } =
+ $input_names[0];
+ }
+ else {
+ $inputs_txt .= $_ . ",";
+ }
+ }
+ }
+
+ $inputs_txt =~ s/\s/_/g;
+ $inputs_txt = substr( $inputs_txt, 0, -1 );
+ }
+
+ # if we could read the actual available inputs from the receiver, use them
+ elsif (defined( $hash->{helper}{receiver} )
+ && ref( $hash->{helper}{receiver} ) eq "HASH"
+ && defined( $hash->{helper}{receiver}{device}{selectorlist}{count} )
+ && $hash->{helper}{receiver}{device}{selectorlist}{count} > 0 )
+ {
+ $inputs_txt = "-," if ( $state ne "on" );
+
+ foreach my $input (
+ sort
+ keys %{ $hash->{helper}{receiver}{device}{selectorlist}{selector} }
+ )
+ {
+ if ( $hash->{helper}{receiver}{device}{selectorlist}{selector}
+ {$input}{value} eq "1"
+ && $hash->{helper}{receiver}{device}{selectorlist}{selector}
+ {$input}{id} !~ /(80)/ )
+ {
+ $inputs_txt .= $input . ",";
+ }
+ }
+
+ $inputs_txt =~ s/\s/_/g;
+ $inputs_txt = substr( $inputs_txt, 0, -1 );
+ }
+
+ # use general list of possible inputs
+ else {
+ # Find out valid inputs
+ my $inputs =
+ ONKYOdb::ONKYO_GetRemotecontrolValue( "1",
+ ONKYOdb::ONKYO_GetRemotecontrolCommand( "1", "input" ) );
+
+ foreach my $input ( sort keys %{$inputs} ) {
+ $inputs_txt .= $input . ","
+ if ( !( $input =~ /^(07|08|09|up|down|query)$/ ) );
+ }
+ $inputs_txt = substr( $inputs_txt, 0, -1 );
+ }
+
+ my $usage =
+ "Unknown argument '"
+ . $a[1]
+ . "', choose one of toggle:noArg on:noArg off:noArg volume:slider,0,1,100 volumeUp:noArg volumeDown:noArg input:"
+ . $inputs_txt;
+ $usage .= " sleep:off,5,10,15,30,60,90" if ( $zone eq "main" );
+ $usage .= " mute:off,on" if ( $state eq "on" );
+ $usage .= " mute:,-" if ( $state ne "on" );
+
+ if ($adminMode) {
+ $usage .= " statusRequest:noArg";
+ $usage .= " remoteControl:noArg";
+ }
+
+ my $cmd = '';
+ my $result;
+
+ # Stop the internal GetStatus-Loop to avoid
+ # parallel/conflicting requests to device
+ RemoveInternalTimer($hash)
+ if ( $a[1] ne "?" );
+
+ readingsBeginUpdate($hash);
+
+ # statusRequest
+ if ( lc( $a[1] ) eq "statusrequest" ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+ delete $hash->{helper}{receiver} if ( $state ne "absent" );
+ ONKYO_AVR_PULL_GetStatus( $hash, 1 ) if ( !defined( $a[2] ) );
+ }
+
+ # toggle
+ elsif ( lc( $a[1] ) eq "toggle" ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+
+ if ( $hash->{READINGS}{power}{VAL} eq "off" ) {
+ $return = ONKYO_AVR_PULL_Set( $hash, $name, "on" );
+ }
+ else {
+ $return = ONKYO_AVR_PULL_Set( $hash, $name, "off" );
+ }
+ }
+
+ # on
+ elsif ( lc( $a[1] ) eq "on" ) {
+ if ( $hash->{READINGS}{state}{VAL} eq "absent" ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1] . " (wakeup)";
+ my $wakeupCmd = AttrVal( $name, "wakeupCmd", "" );
+
+ if ( $wakeupCmd ne "" ) {
+ $wakeupCmd =~ s/\$DEVICE/$name/g;
+
+ if ( $wakeupCmd =~ s/^[ \t]*\{|\}[ \t]*$//g ) {
+ Log3 $name, 4,
+ "ONKYO_AVR_PULL executing wake-up command (Perl): $wakeupCmd";
+ $result = eval $wakeupCmd;
+ }
+ else {
+ Log3 $name, 4,
+ "ONKYO_AVR_PULL executing wake-up command (fhem): $wakeupCmd";
+ $result = fhem $wakeupCmd;
+ }
+ }
+ else {
+ $return =
+ "Device is offline and cannot be controlled at that stage.";
+ }
+ }
+ else {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "power", "on" );
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{power}{VAL} )
+ || $hash->{READINGS}{power}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "power", $result );
+ }
+ if ( !defined( $hash->{READINGS}{state}{VAL} )
+ || $hash->{READINGS}{state}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "state", $result );
+ }
+ }
+ $interval = 2;
+ }
+ }
+
+ # off
+ elsif ( lc( $a[1] ) eq "off" ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+
+ if ( $hash->{READINGS}{state}{VAL} eq "absent" ) {
+ $return =
+ "Device is offline and cannot be controlled at that stage.";
+ }
+ else {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "power", "off" );
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{power}{VAL} )
+ || $hash->{READINGS}{power}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "power", $result );
+ }
+ if ( !defined( $hash->{READINGS}{state}{VAL} )
+ || $hash->{READINGS}{state}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "state", $result );
+ }
+ }
+ $interval = 2;
+ }
+ }
+
+ # sleep
+ elsif ( lc( $a[1] ) eq "sleep" && $zone eq "main" ) {
+ if ( !defined( $a[2] ) ) {
+ $return = "No argument given, choose one of minutes off";
+ }
+ else {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1] . " " . $a[2];
+
+ if ( $hash->{READINGS}{state}{VAL} eq "absent" ) {
+ $return =
+ "Device is offline and cannot be controlled at that stage.";
+ }
+ else {
+ my $_ = $a[2];
+ if ( $_ eq "off" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "sleep", "off" );
+ }
+ elsif ( m/^\d+$/ && $_ > 0 && $_ <= 90 ) {
+ $result =
+ ONKYO_AVR_PULL_SendCommand( $hash, "sleep",
+ ONKYO_AVR_PULL_dec2hex($_) );
+ }
+ else {
+ $return =
+"Argument does not seem to be a valid integer between 0 and 90";
+ }
+
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{sleep}{VAL} )
+ || $hash->{READINGS}{sleep}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "sleep", $result );
+ }
+ }
+ }
+ }
+ }
+
+ # mute
+ elsif ( lc( $a[1] ) eq "mute" || lc( $a[1] ) eq "mutet" ) {
+ if ( defined( $a[2] ) ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1] . " " . $a[2];
+ }
+ else {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+ }
+
+ if ( $hash->{READINGS}{state}{VAL} eq "on" ) {
+ if ( !defined( $a[2] ) || $a[2] eq "toggle" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "mute", "toggle" );
+ }
+ elsif ( lc( $a[2] ) eq "off" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "mute", "off" );
+ }
+ elsif ( lc( $a[2] ) eq "on" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "mute", "on" );
+ }
+ else {
+ $return = "Argument does not seem to be one of on off toogle";
+ }
+
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{mute}{VAL} )
+ || $hash->{READINGS}{mute}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "mute", $result );
+ }
+ }
+ }
+ else {
+ $return = "Device needs to be ON to mute/unmute audio.";
+ }
+ }
+
+ # volume
+ elsif ( lc( $a[1] ) eq "volume" ) {
+ if ( !defined( $a[2] ) ) {
+ $return = "No argument given";
+ }
+ else {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1] . " " . $a[2];
+
+ if ( $hash->{READINGS}{state}{VAL} eq "on" ) {
+ my $_ = $a[2];
+ if ( m/^\d+$/ && $_ >= 0 && $_ <= 100 ) {
+ $result =
+ ONKYO_AVR_PULL_SendCommand( $hash, "volume",
+ ONKYO_AVR_PULL_dec2hex($_) );
+
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{volume}{VAL} )
+ || $hash->{READINGS}{volume}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "volume", $result );
+ }
+
+ if ( !defined( $hash->{READINGS}{mute}{VAL} )
+ || $hash->{READINGS}{mute}{VAL} eq "on" )
+ {
+ readingsBulkUpdate( $hash, "mute", "off" )
+
+ }
+ }
+ }
+ else {
+ $return =
+"Argument does not seem to be a valid integer between 0 and 100";
+ }
+ }
+ else {
+ $return = "Device needs to be ON to adjust volume.";
+ }
+ }
+ }
+
+ # volumeUp/volumeDown
+ elsif ( lc( $a[1] ) =~ /^(volumeup|volumedown)$/ ) {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1];
+
+ if ( $hash->{READINGS}{state}{VAL} eq "on" ) {
+ if ( lc( $a[1] ) eq "volumeup" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "volume", "level-up" );
+ }
+ else {
+ $result =
+ ONKYO_AVR_PULL_SendCommand( $hash, "volume", "level-down" );
+ }
+
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{volume}{VAL} )
+ || $hash->{READINGS}{volume}{VAL} ne $result )
+ {
+ readingsBulkUpdate( $hash, "volume", $result );
+ }
+ }
+ }
+ else {
+ $return = "Device needs to be ON to adjust volume.";
+ }
+ }
+
+ # input
+ elsif ( lc( $a[1] ) eq "input" ) {
+ if ( !defined( $a[2] ) ) {
+ $return = "No input given";
+ }
+ else {
+ Log3 $name, 3, "ONKYO_AVR_PULL set $name " . $a[1] . " " . $a[2];
+
+ if ( $hash->{READINGS}{power}{VAL} eq "off" ) {
+ $return = ONKYO_AVR_PULL_Set( $hash, $name, "on" );
+ }
+
+ if ( $hash->{READINGS}{state}{VAL} eq "on" ) {
+ $result = ONKYO_AVR_PULL_SendCommand( $hash, "input", $a[2] );
+
+ if ( defined($result) ) {
+ if ( !defined( $hash->{READINGS}{input}{VAL} )
+ || $hash->{READINGS}{input}{VAL} ne $a[2] )
+ {
+ readingsBulkUpdate( $hash, "input", $a[2] );
+ }
+ }
+ }
+ else {
+ $return = "Device needs to be ON to change input.";
+ }
+ $interval = 2;
+ }
+ }
+
+ # remoteControl
+ elsif ( lc( $a[1] ) eq "remotecontrol" ) {
+
+ # Reading commands for zone from HASH table
+ my $commands = ONKYOdb::ONKYO_GetRemotecontrolCommand($zone);
+
+ # Output help for commands
+ if ( !defined( $a[2] ) || $a[2] eq "help" ) {
+
+ # Get all commands for zone
+ my $commands_details =
+ ONKYOdb::ONKYO_GetRemotecontrolCommandDetails($zone);
+
+ my $valid_commands =
+"Usage: \n\nValid commands in zone '$zone':\n\n\n"
+ . "COMMAND\t\t\tDESCRIPTION\n\n";
+
+ # For each valid command
+ foreach my $command ( sort keys %{$commands} ) {
+ my $command_raw = $commands->{$command};
+
+ # add command including description if found
+ if ( defined( $commands_details->{$command_raw}{description} ) )
+ {
+ $valid_commands .=
+ $command
+ . "\t\t\t"
+ . $commands_details->{$command_raw}{description} . "\n";
+ }
+
+ # add command only
+ else {
+ $valid_commands .= $command . "\n";
+ }
+ }
+
+ $valid_commands .=
+ "\nTry ' help' to find out well known values.\n\n\n";
+
+ $return = $valid_commands;
+ }
+ else {
+ # return if command cannot be found in HASH table
+ if ( !defined( $commands->{ $a[2] } ) ) {
+ $return = "Invalid command: " . $a[2];
+ }
+ else {
+
+ # Reading values for command from HASH table
+ my $values =
+ ONKYOdb::ONKYO_GetRemotecontrolValue( $zone,
+ $commands->{ $a[2] } );
+
+ # Output help for values
+ if ( !defined( $a[3] ) || $a[3] eq "help" ) {
+
+ # Get all details for command
+ my $command_details =
+ ONKYOdb::ONKYO_GetRemotecontrolCommandDetails( $zone,
+ $commands->{ $a[2] } );
+
+ my $valid_values =
+ "Usage: "
+ . $a[2]
+ . " \n\nWell known values:\n\n\n"
+ . "VALUE\t\t\tDESCRIPTION\n\n";
+
+ # For each valid value
+ foreach my $value ( sort keys %{$values} ) {
+
+ # add value including description if found
+ if ( defined( $command_details->{description} ) ) {
+ $valid_values .=
+ $value
+ . "\t\t\t"
+ . $command_details->{description} . "\n";
+ }
+
+ # add value only
+ else {
+ $valid_values .= $value . "\n";
+ }
+ }
+
+ $valid_values .= "\n\n\n";
+
+ $return = $valid_values;
+ }
+
+ # normal processing
+ else {
+ Log3 $name, 3,
+ "ONKYO_AVR_PULL set $name "
+ . $a[1] . " "
+ . $a[2] . " "
+ . $a[3];
+
+ if ( $hash->{READINGS}{state}{VAL} ne "absent" ) {
+
+ # special power toogle handling
+ if ( $a[2] eq "power"
+ && $a[3] eq "toggle" )
+ {
+ $result = ONKYO_AVR_PULL_Set( $hash, $name, "toggle" );
+ }
+
+ # normal processing
+ else {
+ $result =
+ ONKYO_AVR_PULL_SendCommand( $hash, $a[2], $a[3] );
+ }
+
+ if ( !defined($result) ) {
+ $return =
+ "ERROR: command '"
+ . $a[2] . " "
+ . $a[3]
+ . "' was NOT successful.";
+ }
+ elsif ( $a[3] eq "query" ) {
+ $return = $result;
+ }
+ }
+ else {
+ $return =
+"Device needs to be reachable to be controlled remotely.";
+ }
+ }
+ }
+ }
+ }
+
+ # return usage hint
+ else {
+ $return = $usage;
+ }
+
+ readingsEndUpdate( $hash, 1 );
+
+ # Re-start internal timer
+ InternalTimer( gettimeofday() + $interval, "ONKYO_AVR_PULL_GetStatus", $hash, 1 )
+ if ( $a[1] ne "?" );
+
+ # return result
+ return $return;
+}
+
+###################################
+sub ONKYO_AVR_PULL_Define($$) {
+ my ( $hash, $def ) = @_;
+ my @a = split( "[ \t][ \t]*", $def );
+ my $name = $hash->{NAME};
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_Define()";
+
+ if ( int(@a) < 3 ) {
+ my $msg =
+"Wrong syntax: define ONKYO_AVR_PULL [] [] []";
+ Log3 $name, 4, $msg;
+ return $msg;
+ }
+
+ $hash->{TYPE} = "ONKYO_AVR_PULL";
+
+ my $address = $a[2];
+ $hash->{helper}{ADDRESS} = $address;
+
+ # use fixed port 60128
+ my $port = 60128;
+ $hash->{helper}{PORT} = $port;
+
+ # used zone to control
+ my $zone = "1";
+ if (defined($a[4]) && $a[4] =~ /^[a-zA-Z]*([2-4])$/) {
+ $zone = $1;
+ }
+ $hash->{ZONE} = $zone;
+
+ my $interval;
+ if ( $zone eq "1" ) {
+
+ # use interval of 75sec for main zone if not defined
+ $interval = $a[5] || 75;
+ }
+ else {
+ # use interval of 90sec for other zones if not defined
+ $interval = $a[5] || 90;
+ }
+ $hash->{INTERVAL} = $interval;
+
+ # protocol version
+ my $protocol = $a[3] || 2013;
+ if ( !( $protocol =~ /^(2013|pre2013)$/ ) ) {
+ return "Invalid protocol, choose one of 2013 pre2013";
+ }
+ readingsSingleUpdate( $hash, "deviceyear", $protocol, 1 );
+ if (
+ $protocol eq "pre2013"
+ && ( !exists( $attr{$name}{model} )
+ || $attr{$name}{model} ne $protocol )
+ )
+ {
+ $attr{$name}{model} = $protocol;
+ }
+
+ # check values
+ if ( !( $zone =~ /^(main|[a-zA-Z]*[1-4]|dock)$/ ) ) {
+ return "Invalid zone, choose one of main zone2 zone3 zone4 dock";
+ }
+
+ # set default settings on first define
+ if ($init_done) {
+ $attr{$name}{webCmd} = 'volume:mute:input';
+ $attr{$name}{devStateIcon} =
+ 'on:rc_GREEN:off off:rc_STOP:on absent:rc_RED';
+ }
+ $hash->{helper}{receiver} = undef;
+
+ unless ( exists( $hash->{helper}{AVAILABLE} )
+ and ( $hash->{helper}{AVAILABLE} == 0 ) )
+ {
+ $hash->{helper}{AVAILABLE} = 1;
+ readingsSingleUpdate( $hash, "presence", "present", 1 );
+ }
+
+ # start the status update timer
+ RemoveInternalTimer($hash);
+ InternalTimer( gettimeofday() + 2, "ONKYO_AVR_PULL_GetStatus", $hash, 0 );
+
+ return undef;
+}
+
+############################################################################################################
+#
+# Begin of helper functions
+#
+############################################################################################################
+
+###################################
+sub ONKYO_AVR_PULL_SendCommand($$$) {
+ my ( $hash, $cmd, $value ) = @_;
+ my $name = $hash->{NAME};
+ my $address = $hash->{helper}{ADDRESS};
+ my $port = $hash->{helper}{PORT};
+ my $protocol = $hash->{READINGS}{deviceyear}{VAL};
+ my $zone = $hash->{ZONE};
+ my $timeout = 3;
+ my $response;
+ my $response_code;
+ my $return;
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_SendCommand()";
+
+ # Input alias handling
+ if ( $cmd eq "input" ) {
+
+ # Resolve input alias to correct name
+ if ( defined( $hash->{helper}{receiver}{input_names}{$value} ) ) {
+ $value = $hash->{helper}{receiver}{input_names}{$value};
+ }
+
+ # Resolve device specific input alias
+ $value =~ s/_/ /g;
+ if (
+ defined(
+ $hash->{helper}{receiver}{device}{selectorlist}
+ {selector}{$value}{id}
+ )
+ )
+ {
+ $value = uc( $hash->{helper}{receiver}{device}{selectorlist}
+ {selector}{$value}{id} );
+ }
+ }
+
+ # Resolve command and value to ISCP raw command
+ my $cmd_raw = ONKYOdb::ONKYO_GetRemotecontrolCommand( $zone, $cmd );
+ my $value_raw =
+ ONKYOdb::ONKYO_GetRemotecontrolValue( $zone, $cmd_raw, $value );
+ my $request_code = substr( $cmd_raw, 0, 3 );
+
+ if ( !defined($cmd_raw) ) {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): command '$cmd' is not available within zone '$zone' or command is invalid";
+ return undef;
+ }
+
+ if ( !defined($value_raw) ) {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd - Warning, value '$value' not found in HASH table, will be sent to receiver 'as is'";
+ $value_raw = $value;
+ }
+
+ Log3 $name, 4,
+ "ONKYO_AVR_PULL $name($zone): $cmd -> $value ($cmd_raw$value_raw)";
+
+ my $filehandle = IO::Socket::INET->new(
+ PeerAddr => $address,
+ PeerPort => $port,
+ Proto => 'tcp',
+ Timeout => $timeout,
+ );
+
+ if ( defined($filehandle) && $cmd_raw ne "" && $value_raw ne "" ) {
+ my $str = ONKYO_AVR_PULL_Pack( $cmd_raw . $value_raw, $protocol );
+
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name($zone): $address:$port snd "
+ . ONKYO_AVR_PULL_hexdump($str);
+
+ syswrite $filehandle, $str, length $str;
+
+ my $start_time = time();
+ my $readon = 1;
+ do {
+ my $bytes = ONKYO_AVR_PULL_sysreadline( $filehandle, 1, $protocol );
+
+ my $line = ONKYO_AVR_PULL_read( $hash, \$bytes )
+ if ( defined($bytes) && $bytes ne "" );
+
+ $response_code = substr( $line, 0, 3 ) if defined($line);
+
+ if ( defined($response_code)
+ && $response_code eq $request_code )
+ {
+ $response->{$response_code} = $line;
+ $readon = 0;
+ }
+ elsif ( defined($response_code) ) {
+ $response->{$response_code} = $line;
+ }
+
+ $readon = 0 if time() > ( $start_time + $timeout );
+ } while ($readon);
+
+ # Close socket connections
+ $filehandle->close();
+ }
+
+ readingsBeginUpdate($hash);
+
+ unless ( defined($response) ) {
+ if ( defined( $hash->{helper}{AVAILABLE} )
+ and $hash->{helper}{AVAILABLE} eq 1 )
+ {
+ Log3 $name, 3, "ONKYO_AVR_PULL device $name is unavailable";
+ readingsBulkUpdate( $hash, "presence", "absent" );
+ }
+ $hash->{helper}{AVAILABLE} = 0;
+ }
+ else {
+ if ( defined( $hash->{helper}{AVAILABLE} )
+ and $hash->{helper}{AVAILABLE} eq 0 )
+ {
+ Log3 $name, 3, "ONKYO_AVR_PULL device $name is available";
+ readingsBulkUpdate( $hash, "presence", "present" );
+ }
+ $hash->{helper}{AVAILABLE} = 1;
+
+ # Search for expected answer
+ if ( defined( $response->{$request_code} ) ) {
+ my $_ = substr( $response->{$request_code}, 3 );
+
+ # Decode return value
+ #
+ my $values =
+ ONKYOdb::ONKYO_GetRemotecontrolCommandDetails( $zone,
+ $request_code );
+
+ # Decode through device information
+ if ( $cmd eq "input"
+ && defined( $hash->{helper}{receiver} )
+ && ref( $hash->{helper}{receiver} ) eq "HASH"
+ && defined( $hash->{helper}{receiver}{input}{$_} ) )
+ {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw return value '$_' converted through device information to '"
+ . $hash->{helper}{receiver}{input}{$_} . "'";
+ $return = $hash->{helper}{receiver}{input}{$_};
+ }
+
+ # Decode through HASH table
+ elsif ( defined( $values->{values}{"$_"}{name} ) ) {
+ if ( ref( $values->{values}{"$_"}{name} ) eq "ARRAY" ) {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw return value '$_' converted through ARRAY from HASH table to '"
+ . $values->{values}{"$_"}{name}[0] . "'";
+ $return = $values->{values}{"$_"}{name}[0];
+ }
+ else {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw return value '$_' converted through VALUE from HASH table to '"
+ . $values->{values}{"$_"}{name} . "'";
+ $return = $values->{values}{"$_"}{name};
+ }
+ }
+
+ # return as decimal
+ elsif ( m/^[0-9A-Fa-f][0-9A-Fa-f]$/
+ && $request_code =~ /^(MVL|SLP)$/ )
+ {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw return value '$_' converted from HEX to DEC ";
+ $return = ONKYO_AVR_PULL_hex2dec($_);
+
+ }
+
+ # just return the original return value if there is
+ # no decoding function
+ elsif ( lc($_) ne "n/a" ) {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw unconverted return of value '$_'";
+ $return = $_;
+
+ }
+
+ # Log if the command is not supported by the device
+ elsif ( $value_raw ne "QSTN" ) {
+ Log3 $name, 3,
+"ONKYO_AVR_PULL $name($zone): command $cmd -> $value ($cmd_raw$value_raw) not supported by device";
+ }
+
+ }
+ else {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): No valid response for command '$cmd_raw' during request session of $timeout seconds";
+ }
+
+ # Input alias handling
+ if ( $cmd eq "input"
+ && defined($return)
+ && defined( $hash->{helper}{receiver}{input_aliases}{$return} ) )
+ {
+ Log3 $name, 4,
+"ONKYO_AVR_PULL $name($zone): $cmd_raw$value_raw aliasing '$return' to '"
+ . $hash->{helper}{receiver}{input_aliases}{$return} . "'";
+ $return = $hash->{helper}{receiver}{input_aliases}{$return};
+ }
+
+ # clear hash to free memory
+ %{$response} = ();
+
+ return $return;
+ }
+
+ readingsEndUpdate( $hash, 1 );
+
+ return undef;
+}
+
+###################################
+sub ONKYO_AVR_PULL_sysreadline($;$$) {
+ my ( $handle, $timeout, $protocol ) = @_;
+ $handle = qualify_to_ref( $handle, caller() );
+ my $infinitely_patient = ( @_ == 1 || $timeout < 0 );
+ my $start_time = time();
+ my $selector = IO::Select->new();
+ $selector->add($handle);
+ my $line = "";
+ SLEEP:
+
+ until ( ONKYO_AVR_PULL_at_eol( $line, $protocol ) ) {
+ unless ($infinitely_patient) {
+ return $line if time() > ( $start_time + $timeout );
+ }
+
+ # sleep only 1 second before checking again
+ next SLEEP unless $selector->can_read(1.0);
+ INPUT_READY:
+ while ( $selector->can_read(0.0) ) {
+ my $was_blocking = $handle->blocking(0);
+ CHAR: while ( sysread( $handle, my $nextbyte, 1 ) ) {
+ $line .= $nextbyte;
+ last CHAR if $nextbyte eq "\n";
+ }
+ $handle->blocking($was_blocking);
+
+ # if incomplete line, keep trying
+ next SLEEP unless ONKYO_AVR_PULL_at_eol( $line, $protocol );
+ last INPUT_READY;
+ }
+ }
+ return $line;
+}
+
+###################################
+sub ONKYO_AVR_PULL_at_eol($;$) {
+ if ( $_[0] =~ /\r\n\z/ || $_[0] =~ /\r\z/ ) {
+ return 1;
+ }
+ else {
+ return 0;
+ }
+}
+
+###################################
+sub ONKYO_AVR_PULL_Undefine($$) {
+ my ( $hash, $arg ) = @_;
+ my $name = $hash->{NAME};
+
+ Log3 $name, 5, "ONKYO_AVR_PULL $name: called function ONKYO_AVR_PULL_Undefine()";
+
+ # Stop the internal GetStatus-Loop and exit
+ RemoveInternalTimer($hash);
+ return undef;
+}
+
+###################################
+sub ONKYO_AVR_PULL_read($$) {
+ my ( $hash, $rbuf ) = @_;
+ my $name = $hash->{NAME};
+ my $address = $hash->{helper}{ADDRESS};
+ my $port = $hash->{helper}{PORT};
+ my $zone = $hash->{ZONE};
+
+ return unless ($$rbuf);
+
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name($zone): $address:$port rcv " . ONKYO_AVR_PULL_hexdump($$rbuf);
+
+ my $length = length $$rbuf;
+ return unless ( $length >= 16 );
+
+ my ( $magic, $header_size, $data_size, $version, $res1, $res2, $res3 ) =
+ unpack 'a4 N N C4', $$rbuf;
+
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name: Unexpected magic: expected 'ISCP', got '$magic'"
+ and return
+ unless ( $magic eq 'ISCP' );
+
+ return unless ( $length >= $header_size + $data_size );
+
+ substr $$rbuf, 0, $header_size, '';
+
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name: Unexpected version: expected '0x01', got '0x%02x' "
+ . $version
+ unless ( $version == 0x01 );
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name: Unexpected header size: expected '0x10', got '0x%02x' "
+ . $header_size
+ unless ( $header_size == 0x10 );
+
+ my $body = substr $$rbuf, 0, $data_size, '';
+ my $sd = substr $body, 0, 2, '';
+ $body =~ s/([\032\r\n]|[\032\r]|[\r\n]|[\r])+$//;
+
+ Log3 $name, 5,
+ "ONKYO_AVR_PULL $name: Unexpected start/destination: expected '!1', got '$sd'"
+ unless ( $sd eq '!1' );
+
+ return $body;
+}
+
+###################################
+sub ONKYO_AVR_PULL_Pack($;$) {
+ my ( $d, $protocol ) = @_;
+
+ # ------------------
+ # < 2013 (produced by TX-NR515)
+ # ------------------
+ #
+ # EXAMPLE REQUEST FOR PWRQSTN
+ # 4953 4350 0000 0010 0000 000a 0100 0000 ISCP............
+ # 2131 5057 5251 5354 4e0d !1PWRQSTN.
+ #
+ # EXAMPLE REPLY FOR PWRQSTN
+ # 4953 4350 0000 0010 0000 000a 0100 0000 ISCP............
+ # 2131 5057 5230 311a 0d0a !1PWR01...
+ #
+
+ # ------------------
+ # 2013+ (produced by TX-NR626)
+ # ------------------
+ #
+ # EXAMPLE REQUEST FOR PWRQSTN
+ # 4953 4350 0000 0010 0000 000b 0100 0000 ISCP............
+ # 2131 5057 5251 5354 4e0d 0a !1PWRQSTN..
+ #
+ # EXAMPLE REPLY FOR PWRQSTN
+ # 4953 4350 0000 0010 0000 000a 0100 0000 ISCP............
+ # 2131 5057 5230 311a 0d0a !1PWR01...
+ #
+
+ # add start character and destination unit type 1=receiver
+ $d = '!1' . $d;
+
+ # If protocol is defined as pre-2013 use EOF code for older models
+ if ( defined($protocol) && $protocol eq "pre2013" ) {
+
+ # = 0x0d
+ $d .= "\r";
+ }
+
+ # otherwise use EOF code for newer models
+ else {
+
+ # = 0x0d0a
+ $d .= "\r\n";
+ }
+
+ pack( "a* N N N a*", 'ISCP', 0x10, ( length $d ), 0x01000000, $d );
+}
+
+###################################
+sub ONKYO_AVR_PULL_hexdump {
+ my $s = shift;
+ my $r = unpack 'H*', $s;
+ $s =~ s/[^ -~]/./g;
+ $r . ' ' . $s;
+}
+
+###################################
+sub ONKYO_AVR_PULL_hex2dec($) {
+ my ($hex) = @_;
+ return unpack( 's', pack 's', hex($hex) );
+}
+
+###################################
+sub ONKYO_AVR_PULL_dec2hex($) {
+ my ($dec) = @_;
+ my $hex = uc( sprintf( "%x", $dec ) );
+
+ return "0" . $hex if ( length($hex) eq 1 );
+ return $hex;
+}
+
+#####################################
+# Callback from 95_remotecontrol for command makenotify.
+sub ONKYO_AVR_PULL_RCmakenotify($$) {
+ my ( $name, $ndev ) = @_;
+ my $nname = "notify_$name";
+
+ fhem( "define $nname notify $name set $ndev remoteControl " . '$EVENT', 1 );
+ Log3 undef, 2, "[remotecontrol:ONKYO_AVR_PULL] Notify created: $nname";
+ return "Notify created by ENIGMA2: $nname";
+}
+
+#####################################
+# RC layouts
+
+sub ONKYO_AVR_PULL_RClayout_SVG() {
+ my @row;
+
+ $row[0] = ":rc_BLANK.svg,:rc_BLANK.svg,power toggle:rc_POWER.svg";
+ $row[1] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg";
+
+ $row[2] = "1:rc_1.svg,2:rc_2.svg,3:rc_3.svg";
+ $row[3] = "4:rc_4.svg,5:rc_5.svg,6:rc_6.svg";
+ $row[4] = "7:rc_7.svg,8:rc_8.svg,9:rc_9.svg";
+ $row[5] = ":rc_BLANK.svg,0:rc_0.svg,:rc_BLANK.svg";
+ $row[6] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg";
+
+ $row[7] = "VOLUMEUP:rc_VOLPLUS.svg,MUTE:rc_MUTE.svg,CHANNELUP:rc_UP.svg";
+ $row[8] =
+ "VOLUMEDOWN:rc_VOLMINUS.svg,EXIT:rc_EXIT.svg,CHANNELDOWN:rc_DOWN.svg";
+ $row[9] = ":rc_BLANK.svg,:rc_BLANK.svg,:rc_BLANK.svg";
+
+ $row[10] = "INFO:rc_INFO.svg,UP:rc_UP.svg,MENU:rc_MENU.svg";
+ $row[11] = "LEFT:rc_LEFT.svg,OK:rc_OK.svg,RIGHT:rc_RIGHT.svg";
+ $row[12] = "AUDIO:rc_AUDIO.svg,DOWN:rc_DOWN.svg,VIDEO:rc_VIDEO.svg";
+ $row[13] = ":rc_BLANK.svg,EXIT:rc_EXIT.svg,:rc_BLANK.svg";
+
+ $row[14] =
+"RED:rc_REWred.svg,GREEN:rc_PLAYgreen.svg,YELLOW:rc_PAUSEyellow.svg,BLUE:rc_FFblue.svg";
+ $row[15] =
+"TV:rc_TVstop.svg,RADIO:rc_RADIOred.svg,TEXT:rc_TEXT.svg,HELP:rc_HELP.svg";
+
+ $row[16] = "attr rc_iconpath icons/remotecontrol";
+ $row[17] = "attr rc_iconprefix black_btn_";
+ return @row;
+}
+
+sub ONKYO_AVR_PULL_RClayout() {
+ my @row;
+
+ $row[0] = ":blank,:blank,power toggle:POWEROFF";
+ $row[1] = ":blank,:blank,:blank";
+
+ $row[2] = "1,2,3";
+ $row[3] = "4,5,6";
+ $row[4] = "7,8,9";
+ $row[5] = ":blank,0:0,:blank";
+ $row[6] = ":blank,:blank,:blank";
+
+ $row[7] = "VOLUMEUP:VOLUP,MUTE,CHANNELUP:CHUP2";
+ $row[8] = "VOLUMEDOWN:VOLDOWN,EXIT,CHANNELDOWN:CHDOWN2";
+ $row[9] = ":blank,:blank,:blank";
+
+ $row[10] = "INFO,UP,MENU";
+ $row[11] = "LEFT,OK,RIGHT";
+ $row[12] = "AUDIO,DOWN,VIDEO";
+ $row[13] = ":blank,:blank,:blank";
+
+ $row[14] = "RED:REWINDred,GREEN:PLAYgreen,YELLOW:PAUSEyellow,BLUE:FFblue";
+ $row[15] = "TV:TVstop,RADIO:RADIOred,TEXT,HELP";
+
+ $row[16] = "attr rc_iconpath icons/remotecontrol";
+ $row[17] = "attr rc_iconprefix black_btn_";
+ return @row;
+}
+
+1;
+
+=pod
+=item device
+=begin html
+
+
+
+
+
+ ONKYO_AVR_PULL
+
+
+ Define
+
+ define <name> ONKYO_AVR_PULL <ip-address-or-hostname> [<protocol-version>] [<zone>] [<poll-interval>]
+
+ This module controls ONKYO A/V receivers via network connection.
+
+ Defining an ONKYO device will schedule an internal task (interval can be set with optional parameter <poll-interval> in seconds, if not set, the value is 75 seconds), which periodically reads the status of the device and triggers notify/filelog commands.
+
+ Example:
+
+ define avr ONKYO_AVR_PULL 192.168.0.10
+
+ # With explicit protocol version 2013 and later
+ define avr ONKYO_AVR_PULL 192.168.0.10 2013
+
+ # With protocol version prior 2013
+ define avr ONKYO_AVR_PULL 192.168.0.10 pre2013
+
+ # With zone2
+ define avr ONKYO_AVR_PULL 192.168.0.10 pre2013 zone2
+
+ # With custom interval of 60 seconds
+ define avr ONKYO_AVR_PULL 192.168.0.10 pre2013 main 60
+
+ # With zone2 and custom interval of 60 seconds
+ define avr ONKYO_AVR_PULL 192.168.0.10 pre2013 zone2 60
+
+
+
+ Set
+
+ set <name> <command> [<parameter>]
+
+ Currently, the following commands are defined (may vary depending on zone).
+
+ -
+ on - powers on the device
+
+ -
+ off - turns the device in standby mode
+
+ -
+ sleep 1..90,off - sets auto-turnoff after X minutes
+
+ -
+ toggle - switch between on and off
+
+ -
+ volume 0...100 - set the volume level in percentage
+
+ -
+ volumeUp - increases the volume level
+
+ -
+ volumeDown - decreases the volume level
+
+ -
+ mute on,off - controls volume mute
+
+ -
+ input - switches between inputs
+
+ -
+ statusRequest - requests the current status of the device
+
+ -
+ remoteControl - sends remote control commands; see remoteControl help
+
+
+
+ Note: If you would like to restrict access to admin set-commands (-> statusRequest, remoteControl) you may set your FHEMWEB instance's attribute allowedCommands like 'set,set-user'.
+ The string 'set-user' will ensure only non-admin set-commands can be executed when accessing FHEM using this FHEMWEB instance.
+
+
+
+ Get
+
+ get <name> <what>
+
+ Currently, the following commands are defined (may vary depending on zone):
+
+
+ power
+ input
+ volume
+ mute
+ sleep
+
+
+
+ Generated Readings/Events (may vary depending on zone):
+
+ -
+ input - Shows currently used input; part of FHEM-4-AV-Devices compatibility
+
+ -
+ mute - Reports the mute status of the device (can be "on" or "off")
+
+ -
+ power - Reports the power status of the device (can be "on" or "off")
+
+ -
+ presence - Reports the presence status of the receiver (can be "absent" or "present"). In case of an absent device, control is not possible.
+
+ -
+ sleep - Reports current sleep state (can be "off" or shows timer in minutes)
+
+ -
+ state - Reports current power state and an absence of the device (can be "on", "off" or "absent")
+
+ -
+ volume - Reports current volume level of the receiver in percentage values (between 0 and 100 %)
+
+
+
+
+=end html
+
+=begin html_DE
+
+
+
+
+
+ ONKYO_AVR_PULL
+
+
+ Eine deutsche Version der Dokumentation ist derzeit nicht vorhanden. Die englische Version ist hier zu finden:
+
+
+
+=end html_DE
+
+=cut