From 2bdefd267bd09195ba21d77e6765158d1ee64462 Mon Sep 17 00:00:00 2001 From: Beta-User <> Date: Thu, 27 Oct 2022 04:08:31 +0000 Subject: [PATCH] 96_Snapcast: possible fix for mute problem git-svn-id: https://svn.fhem.de/fhem/trunk@26597 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/96_Snapcast.pm | 151 ++++++++++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 41 deletions(-) diff --git a/fhem/FHEM/96_Snapcast.pm b/fhem/FHEM/96_Snapcast.pm index 311827110..3d1d1065a 100755 --- a/fhem/FHEM/96_Snapcast.pm +++ b/fhem/FHEM/96_Snapcast.pm @@ -31,7 +31,7 @@ use Time::HiRes qw( gettimeofday ); use DevIo; use JSON (); use GPUtils qw( GP_Import ); -use List::Util qw( max min ); +use List::Util 1.45 qw( max min uniq ); #-- Run before package compilation @@ -85,7 +85,8 @@ my %_clientmethods = ( volume => 'Client.SetVolume', mute => 'Client.SetVolume', stream => 'Group.SetStream', - latency => 'Client.SetLatency' + latency => 'Client.SetLatency', + group => 'Group.SetClients' ); =pod @@ -262,38 +263,30 @@ sub Set { } return; } - my @ids = grep {m{\A(?:clients_.+_group)\z}xms} - keys %{ $hash->{READINGS} }; - my @group; - for my $sid ( @ids ) { - my $gr = ReadingsVal($name, $sid, ''); - if ( $client eq $gr ) { - $sid =~ m{\Aclients_(.+)_group\z}xms; - push @group, $1; - }; - #clients_84a93e695051_2_group a269028b-7078-210f-0e75-54acd507faaa - } - Log3( $hash, 5, "Snap: group members for arg. $client are @group within @ids"); - if ( @group ) { + + my $grp = _getGroupMembers($hash, $client); + if ( @{$grp} ) { + my $sparm; + my @paramset; if ( $opt eq 'volume' && looks_like_number($value) && $value !~ m{[+-]}x ) { #Log3($hash,3,"SNAP: Group absolute volume command, volume: $value"); - my @paramset; my $grvol; - for my $sclient ( @group ) { + for my $sclient ( @{$grp} ) { $grvol += ReadingsNum( $name, "clients_${sclient}_volume", 0); } - $grvol = int $grvol/@group; + $grvol = int $grvol/@{$grp}; my $change = $value - $grvol; - for my $sclient ( @group ) { - my $sparm->{id} = ReadingsVal( $name, "clients_${sclient}_origid", undef) // next;#_getId( $hash, $sclient) // next; + for my $sclient ( @{$grp} ) { + $sparm->{id} = ReadingsVal( $name, "clients_${sclient}_origid", undef) // next;#_getId( $hash, $sclient) // next; my $vol = ReadingsNum( $name, "clients_${sclient}_volume", 0) + $change; $vol = max( 0, min( 100, $vol ) ); my $muteState = ReadingsVal( $name, "clients_${sclient}_muted", 'false' ); $muteState = 'false' if $vol && ( $muteState eq 'true' || $muteState eq '1' ); $sparm->{volume}->{muted} = $muteState; $sparm->{volume}->{percent} = $vol; - my $payload = push @paramset, Snapcast_Encode( $hash, $_clientmethods{volume}, $sparm); + push @paramset, Snapcast_Encode( $hash, $_clientmethods{volume}, $sparm, 1); } + return if !@paramset; my $payload = q{[}; $payload .= join q{,},@paramset; @@ -301,7 +294,30 @@ sub Set { #Log3($hash,3,"SNAP: send batch $payload"); return DevIo_SimpleWrite( $hash, $payload, 2 ); } - for my $sclient ( @group ) { + if ( $opt eq 'group' ) { + Log3( $hash, 3, "Snap: $opt command received for @{$grp}" ); + my $opt2 = shift @param; + my $clnt = shift @param // return 'group commands require two additional arguments!'; + $clnt = _getId( $hash, $clnt) // $clnt; + $sparm->{id} = $client; #needs group2id function as well + + if ( $opt2 eq 'name' ) { + $sparm->{name} = $clnt; + + return DevIo_SimpleWrite( $hash, Snapcast_Encode( $hash, 'Group.SetName', $sparm), 2 ); + } + push @{$grp}, $clnt if $opt2 eq 'add'; + my @grIds; + for ( @{$grp} ) { + push @grIds, _getId( $hash, $_) // $_; + } + @grIds = grep { $_ !~ m{\A$clnt\z}x } @grIds if $opt2 eq 'remove'; + @grIds = uniq(@grIds); + $sparm->{clients} = \@grIds; + return DevIo_SimpleWrite( $hash, Snapcast_Encode( $hash, 'Group.SetClients', $sparm), 2 ); + } + + for my $sclient ( @{$grp} ) { $sclient =~ s{:}{}gx; $sclient =~ s{[#]}{_}gx; my $res = _setClient( $hash, $sclient, $opt, $value ); @@ -316,6 +332,20 @@ sub Set { return "$opt not implemented yet!"; } +=pod + + For client type devices, you may use a single group id as argument to add the client to the given group or the keyword remove to singularize that client. + Options for server type devices: + + +Group.SetClients +{"id":3,"jsonrpc":"2.0","method":"Group.SetClients","params":{"clients":["00:21:6a:7d:74:fc#2","00:21:6a:7d:74:fc"],"id":"4dcc4e3b-c699-a04b-7f0c-8260d23c43e1"}} +=cut + sub Read { my $hash = shift // return; my $name = $hash->{NAME}; @@ -428,10 +458,12 @@ sub Read { readingsBeginUpdate($hash); readingsBulkUpdateIfChanged( $hash, "clients_${client}_$key", $update->{result} ); readingsEndUpdate( $hash, 1 ); - my $clienthash = $defs{$hash->{$client}} // return; - readingsBeginUpdate($clienthash); - readingsBulkUpdateIfChanged( $clienthash, $key, $update->{result} ); - readingsEndUpdate( $clienthash, 1 ); + if ( defined $hash->{$client} ) { + my $clienthash = $defs{$hash->{$client}} // return; + readingsBeginUpdate($clienthash); + readingsBulkUpdateIfChanged( $clienthash, $key, $update->{result} ); + readingsEndUpdate( $clienthash, 1 ); + } } } } @@ -741,7 +773,7 @@ sub _setClient { if ( $param eq 'stream' ) { $paramset->{id} = ReadingsVal( $name, "clients_${id}_group", "" ); # for setting stream we now use group id instead of client id in snapcast 0.11 JSON format $param = 'stream_id'; - if ( $value eq "next" ) { # just switch to the next stream, if last stream, jump to first one. This way streams can be cycled with a button press + if ( $value eq 'next' ) { # just switch to the next stream, if last stream, jump to first one. This way streams can be cycled with a button press my $totalstreams = ReadingsVal( $name, 'streams', 0 ); my $currentstream = _getStreamNumber( $hash, ReadingsVal( $name, "clients_${id}_stream_id", '' ) ); my $newstream = $currentstream + 1; @@ -774,7 +806,8 @@ sub _setClient { $value = $currentVol - $step; } $value = max( 0, min( 100, $value ) ); - $muteState = 'false' if ( $value > 0 && ( $muteState eq 'true' || $muteState == 1 )); + $muteState = 'false' if ( $value > 0 && ( $muteState eq 'true' || $muteState ne '1' )); + $volumeobject->{muted} = $muteState; } return if !looks_like_number($value); $volumeobject->{percent} = $value + 0; @@ -783,14 +816,16 @@ sub _setClient { if ( $param eq 'mute' ) { $volumeobject->{percent} = $currentVol + 0; - $value = $volumeobject; - - if ( !defined $value->{muted} || $value->{muted} eq '' ) { - $value = $muteState eq 'true' || $muteState == 1 ? 'false' : 'true'; + if ( $value eq 'true' || $value eq 'false' ) { $volumeobject->{muted} = $value; - $volumeobject->{percent} = $currentVol + 0; - $value = $volumeobject; + } else { + $value = $volumeobject; + if ( !defined $value->{muted} || $value->{muted} eq '' ) { + $value = $muteState eq 'true' || $muteState == 1 ? 'false' : 'true'; + $volumeobject->{muted} = $value; + } } + $value = $volumeobject; $param = 'volume'; # change param to "volume" to match new format } @@ -810,8 +845,7 @@ sub Snapcast_Do { my $method = shift // return; my $param = shift // ''; my $payload = Snapcast_Encode( $hash, $method, $param ); - $payload .= "\r\n"; - + #Log3($hash,3,"SNAP: Do $payload"); return DevIo_SimpleWrite( $hash, $payload, 2 ); } @@ -820,7 +854,8 @@ sub Snapcast_Encode { my $hash = shift // return; my $method = shift // return; my $param = shift // ''; - + my $nonl = shift; + if ( defined( $hash->{helper}{REQID} ) ) { $hash->{helper}{REQID}++; } else { $hash->{helper}{REQID} = 1; } $hash->{helper}{REQID} = 1 if $hash->{helper}{REQID} > 16383; # not sure if this is needed but we better dont let this grow forever @@ -834,6 +869,7 @@ sub Snapcast_Encode { $request->{id} = $request->{id} + 0; $json = JSON->new->encode($request); $json =~ s{("(true|false|null)")}{$2}gxms; + $json .= "\r\n" if !$nonl; return $json; } @@ -847,6 +883,25 @@ sub _getStreamNumber { return; } +sub _getGroupMembers { + my $hash = shift // return; + my $grid = shift // return; + my $name = $hash->{NAME} // return; + + my @ids = grep {m{\A(?:clients_.+_group)\z}xms} + keys %{ $hash->{READINGS} }; + my @group; + for my $sid ( @ids ) { + my $gr = ReadingsVal($name, $sid, ''); + if ( $grid eq $gr ) { + $sid =~ m{\Aclients_(.+)_group\z}xms; + push @group, $1; + }; + } + Log3( $hash, 5, "Snap: group members for arg. $grid are @group within @ids"); + return \@group; +} + sub _getId { my $hash = shift // return; my $client = shift // return; @@ -854,7 +909,7 @@ sub _getId { my $name = $hash->{NAME} // return; # client is ID - if ( $client =~ m/^([0-9a-f]{12}([#_]*\d*|$))$/i ) { +if ( $client =~ m{\A(?:[[:xdigit:]]{8}-(?:[[:xdigit:]]{4}-){3}[[:xdigit:]]{12}(?:[#_]*\d*)|[[:xdigit:]]{12}(?:[#_]*\d*))\z}i ) { for my $i ( 1 .. ReadingsVal( $name, 'streams', 1 ) ) { return $hash->{STATUS}->{clients}->{$i}->{origid} if $client eq $hash->{STATUS}->{clients}->{$i}->{id}; @@ -954,9 +1009,14 @@ __END__
  • update
    Perform a full update of the Snapcast Status including streams and servers. Only needed if something is not working. Server module only
  • volume
    - Set the volume of a client. For this and all the following 4 options, give client as second parameter (only for the server module), either as name, IP , or MAC and the desired value as third parameter. - Client can be given as "all", in that case all clients are changed at once (only for server module)
    - Volume can be given in 3 ways: Range between 0 and 100 to set volume directly. Increment or Decrement given between -100 and +100. Keywords up and down to increase or decrease with a predifined step size. + Set the volume of a client. For this and all the following 4 options, give target as second parameter (only for the server module) and the desired value as third parameter. + target can be given as + + volume can be given in 3 ways: Range between 0 and 100 to set volume directly. Increment or Decrement given between -100 and +100. Keywords up and down to increase or decrease with a predifined step size. The step size can be defined in the attribute volumeStepSize
    The step size can be defined smaller for the lower volume range, so that finetuning is possible in this area. See the description of the attributes volumeStepSizeSmall and volumeStepThreshold @@ -970,6 +1030,15 @@ __END__
  • stream
    Change the stream that the client is listening to. Snapcast uses one or more streams which can be unterstood as virtual audio channels. Each client/room can subscribe to one of them. By using next as value, you can cycle through the avaialble streams
  • +
  • group
    + For client type devices, you may use a single group id as argument to add the client to the given group or the keyword remove to singularize that client. + Options for server type devices: + +