2
0
mirror of https://github.com/fhem/fhem-mirror.git synced 2025-02-28 02:44:53 +00:00

96_Snapcast: prevent FHEM crash in case of ext. group commands; packaged code

git-svn-id: https://svn.fhem.de/fhem/trunk@26230 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
Beta-User 2022-07-15 06:19:51 +00:00
parent e5cc65eff3
commit 5337f4c645
2 changed files with 238 additions and 110 deletions

View File

@ -1,5 +1,6 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # 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. # Do not insert empty lines here, update check depends on it.
- bugfix: 96_Snapcast: prevent crash in case of external group commands
- feature: 70_ESCVP21net: added undocumented settings (IMGPROC, IRIS, LIRIS) - feature: 70_ESCVP21net: added undocumented settings (IMGPROC, IRIS, LIRIS)
- feature: 30_HUEBridge, 31_HUEDevice: support for Hue tap dial switch - feature: 30_HUEBridge, 31_HUEDevice: support for Hue tap dial switch
- change: 96_Snapcast: PBP code restructured (part I) - change: 96_Snapcast: PBP code restructured (part I)

View File

@ -22,14 +22,46 @@
# The module uses DevIo for communication. There is no blocking communication whatsoever. # The module uses DevIo for communication. There is no blocking communication whatsoever.
# Communication to Snapcast goes through a TCP Socket, Writing and Reading are managed asynchronously. # Communication to Snapcast goes through a TCP Socket, Writing and Reading are managed asynchronously.
package main; package FHEM::Media::Snapcast; ## no critic 'Package declaration'
use strict; use strict;
use warnings; use warnings;
use Scalar::Util qw(looks_like_number); use Scalar::Util qw(looks_like_number);
use Time::HiRes qw(gettimeofday);
use DevIo; use DevIo;
use JSON; use JSON ();
use GPUtils qw(GP_Import);
use List::Util qw( max min );
my %Snapcast_sets = (
#-- Run before package compilation
BEGIN {
# Import from main context
GP_Import(
qw(
defs
init_done
readingFnAttributes
readingsSingleUpdate
readingsBeginUpdate readingsEndUpdate
readingsBulkUpdate readingsBulkUpdateIfChanged
readingsDelete
AttrVal InternalVal
ReadingsVal ReadingsNum
Log3
InternalTimer RemoveInternalTimer
devspec2array
FmtDateTime
DevIo_OpenDev DevIo_CloseDev DevIo_SimpleRead DevIo_SimpleWrite
trim
)
);
}
sub ::Snapcast_Initialize { goto &Initialize }
my %_sets = (
update => 0, update => 0,
volume => 2, volume => 2,
stream => 2, stream => 2,
@ -39,7 +71,7 @@ my %Snapcast_sets = (
group => 2, group => 2,
); );
my %Snapcast_client_sets = ( my %_client_sets = (
volume => 1, volume => 1,
stream => 1, stream => 1,
name => 1, name => 1,
@ -48,7 +80,7 @@ my %Snapcast_client_sets = (
group => 1, group => 1,
); );
my %Snapcast_clientmethods = ( my %_clientmethods = (
name => 'Client.SetName', name => 'Client.SetName',
volume => 'Client.SetVolume', volume => 'Client.SetVolume',
mute => 'Client.SetVolume', mute => 'Client.SetVolume',
@ -56,21 +88,20 @@ my %Snapcast_clientmethods = (
latency => 'Client.SetLatency' latency => 'Client.SetLatency'
); );
sub Snapcast_Initialize { sub Initialize {
my $hash = shift // return; my $hash = shift // return;
$hash->{DefFn} = \&Snapcast_Define; $hash->{DefFn} = \&Define;
$hash->{UndefFn} = \&Snapcast_Undef; $hash->{UndefFn} = \&Undef;
$hash->{SetFn} = \&Snapcast_Set; $hash->{SetFn} = \&Set;
$hash->{GetFn} = \&Snapcast_Get; $hash->{GetFn} = \&Get;
$hash->{WriteFn} = \&Snapcast_Write; $hash->{ReadyFn} = \&Ready;
$hash->{ReadyFn} = \&Snapcast_Ready; $hash->{AttrFn} = \&Attr;
$hash->{AttrFn} = \&Snapcast_Attr; $hash->{ReadFn} = \&Read;
$hash->{ReadFn} = \&Snapcast_Read;
$hash->{AttrList} = "streamnext:all,playing constraintDummy constraints disable:1 volumeStepSize volumeStepSizeSmall volumeStepSizeThreshold $readingFnAttributes"; $hash->{AttrList} = "streamnext:all,playing constraintDummy constraints disable:1 volumeStepSize volumeStepSizeSmall volumeStepSizeThreshold $readingFnAttributes";
return; return;
} }
sub Snapcast_Define { sub Define {
my $hash = shift // return; my $hash = shift // return;
my $def = shift // return; my $def = shift // return;
my @arr = split m{\s+}xms, $def; my @arr = split m{\s+}xms, $def;
@ -84,7 +115,7 @@ sub Snapcast_Define {
readingsSingleUpdate( $hash, 'state', 'defined', 1 ); readingsSingleUpdate( $hash, 'state', 'defined', 1 );
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
DevIo_CloseDev($hash); DevIo_CloseDev($hash);
return Snapcast_Client_Register_Server($hash); return Client_Register_Server($hash);
} }
$hash->{ip} = $arr[1] // 'localhost'; $hash->{ip} = $arr[1] // 'localhost';
$hash->{port} = $arr[2] // '1705'; $hash->{port} = $arr[2] // '1705';
@ -105,10 +136,10 @@ sub Snapcast_Connect {
InternalTimer( gettimeofday() + 5, \&Snapcast_Connect, $hash, 0 ); InternalTimer( gettimeofday() + 5, \&Snapcast_Connect, $hash, 0 );
return; # "init not done"; return; # "init not done";
} }
return DevIo_OpenDev( $hash, 0, \&Snapcast_onConnect, ); return DevIo_OpenDev( $hash, 0, \&onConnect, );
} }
sub Snapcast_Attr { sub Attr {
my $cmd = shift; my $cmd = shift;
my $name = shift; my $name = shift;
my $attr = shift // return; my $attr = shift // return;
@ -126,26 +157,26 @@ sub Snapcast_Attr {
return; return;
} }
sub Snapcast_Undef { sub Undef {
my $hash = shift // return; my $hash = shift // return;
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
DevIo_CloseDev($hash); DevIo_CloseDev($hash);
Snapcast_Client_Unregister_Server($hash) if $hash->{MODE} eq 'client'; Client_Unregister_Server($hash) if $hash->{MODE} eq 'client';
return; return;
} }
sub Snapcast_Get { sub Get {
return 'get is not supported by this module'; return 'get is not supported by this module';
} }
sub Snapcast_Set { sub Set {
my ( $hash, @param ) = @_; my ( $hash, @param ) = @_;
return '"set Snapcast" needs at least one argument' if int @param < 2; return '"set Snapcast" needs at least one argument' if int @param < 2;
my $name = shift @param; my $name = shift @param;
my $opt = shift @param; my $opt = shift @param;
# my $clientmod; # my $clientmod;
my %snap_sets = $hash->{MODE} eq 'client' ? %Snapcast_client_sets : %Snapcast_sets; my %snap_sets = $hash->{MODE} eq 'client' ? %_client_sets : %_sets;
if ( !defined $snap_sets{$opt} ) { if ( !defined $snap_sets{$opt} ) {
my @cList = keys %snap_sets; my @cList = keys %snap_sets;
return "Unknown argument $opt, choose one of " . join( q{ }, @cList ); return "Unknown argument $opt, choose one of " . join( q{ }, @cList );
@ -157,7 +188,7 @@ sub Snapcast_Set {
Snapcast_getStatus($hash); Snapcast_getStatus($hash);
return; return;
} }
if ( defined $Snapcast_clientmethods{$opt} ) { if ( defined $_clientmethods{$opt} ) {
my $client; my $client;
Log3( $hash, 5, "snap: $opt command received" ); Log3( $hash, 5, "snap: $opt command received" );
if ( $hash->{MODE} eq 'client' ) { if ( $hash->{MODE} eq 'client' ) {
@ -178,28 +209,75 @@ sub Snapcast_Set {
if ( $client eq 'all' ) { if ( $client eq 'all' ) {
for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) { for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) {
my $sclient = $hash->{STATUS}->{clients}->{$i}->{host}->{mac}; #my $sclient = $hash->{STATUS}->{clients}->{$i}->{host}->{mac};
$sclient =~ s/\://g; #$sclient =~ s/\://g; #$sclient =~ s{:}{}g; $client =~ s{#}{_}g;
my $res = Snapcast_setClient( $hash, $sclient, $opt, $value ); my $sclient = $hash->{STATUS}->{clients}->{$i}->{id};
my $res = _setClient( $hash, $sclient, $opt, $value );
readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res; readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res;
} }
return; return;
} }
my $res = Snapcast_setClient( $hash, $client, $opt, $value ); my @ids = grep {m{\A(?:clients_.+_group)\z}xms}
keys %{ $hash->{READINGS} };
my @group;
for my $sid ( @ids ) {
#= devspec2array("TYPE=Snapcast:FILTER=group=$client");
next if ReadingsVal($name, $_, '') ne $client;
#clients_84a93e695051#2_group 207ff3bc-5082-789c-c444-29471f4ce57e
if ( $sid =~ m{\Aclients_(.+)_group\z}xms ) {
push @group, $1;
}
}
if ( @group ) {
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 ) {
$grvol += ReadingsNum( $name, "clients_${sclient}_volume", 0);
}
$grvol = int $grvol/@group;
my $change = $value - $grvol;
for my $sclient ( @group ) {
my $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);
}
return if !@paramset;
my $payload = q{[};
$payload .= join q{,},@paramset;
$payload .= q{]}; #q{]\r\n};
Log3($hash,3,"SNAP: send batch $payload");
return DevIo_SimpleWrite( $hash, $payload, 2 );
}
for my $sclient ( @group ) {
$sclient =~ s{:}{}gx;
$sclient =~ s{[#]}{_}gx;
my $res = _setClient( $hash, $sclient, $opt, $value );
readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res;
}
return;
}
my $res = _setClient( $hash, $client, $opt, $value );
readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res; readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res;
return; return;
} }
return "$opt not implemented"; return "$opt not implemented yet!";
} }
sub Snapcast_Read { sub Read {
my $hash = shift // return; my $hash = shift // return;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $buf; my $buf;
$buf = DevIo_SimpleRead($hash); $buf = DevIo_SimpleRead($hash);
return '' if !defined $buf; return '' if !defined $buf;
$buf = $hash->{PARTIAL} . $buf; $buf = $hash->{PARTIAL} . $buf;
$buf =~ s/\r//g; $buf =~ s{\r}{}gx;
my $lastchr = substr( $buf, -1, 1 ); my $lastchr = substr( $buf, -1, 1 );
if ( $lastchr ne "\n" ) { if ( $lastchr ne "\n" ) {
@ -215,11 +293,12 @@ sub Snapcast_Read {
my @lines = split m{\n}x, $buf; my @lines = split m{\n}x, $buf;
for my $line (@lines) { for my $line (@lines) {
Log3( $name, 4, "Snapcast single line is: $line" );
# Hier die Results parsen # Hier die Results parsen
my $decoded_json; my $update;
if (!eval { if (!eval {
$decoded_json = decode_json($line); $update = JSON->new->decode($line);
1; 1;
} }
) )
@ -229,24 +308,34 @@ sub Snapcast_Read {
readingsSingleUpdate( $hash, 'lastError', "Invalid JSON: $buf", 1 ); readingsSingleUpdate( $hash, 'lastError', "Invalid JSON: $buf", 1 );
return; return;
} }
my $update = $decoded_json; if ( ref $update eq 'ARRAY' ) {
if ( defined $hash->{IDLIST} && defined $hash->{IDLIST}->{ $update->{id} } ) { for my $elem ( @{$update} ) {
updateClientInGroup($hash,$elem);
}
return;
}
return if ref $update ne 'HASH';
my $s = $update->{params}->{data};
if ( defined $hash->{IDLIST} && $update->{id} && defined $hash->{IDLIST}->{ $update->{id} } ) {
my $id = $update->{id}; my $id = $update->{id};
#Log3 $name,2, "id: $id "; #Log3 $name,2, "id: $id ";
if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.GetStatus' ) { if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.GetStatus' ) {
delete $hash->{IDLIST}->{$id}; delete $hash->{IDLIST}->{$id};
return Snapcast_parseStatus( $hash, $update ); return parseStatus( $hash, $update );
} }
if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.DeleteClient' ) { if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.DeleteClient' ) {
delete $hash->{IDLIST}->{$id}; delete $hash->{IDLIST}->{$id};
return; return;
} }
while ( my ( $key, $value ) = each %Snapcast_clientmethods ) { while ( my ( $key, $value ) = each %_clientmethods ) {
if ( $value eq $hash->{IDLIST}->{$id}->{method} && $key ne 'mute' ) { #exclude mute here because muting is now integrated in SetVolume if ( $value eq $hash->{IDLIST}->{$id}->{method} && $key ne 'mute' ) { #exclude mute here because muting is now integrated in SetVolume
my $client = $hash->{IDLIST}->{$id}->{params}->{id}; my $client = $hash->{IDLIST}->{$id}->{params}->{id};
$client =~ s/\://g; $client =~ s{:}{}gx;
$client =~ s{[#]}{_}gx;
Log3( $name, 5, "client: $client, key: $key, value: $value" ); Log3( $name, 5, "client: $client, key: $key, value: $value" );
if ( $key eq 'volume' ) { if ( $key eq 'volume' ) {
@ -259,7 +348,7 @@ sub Snapcast_Read {
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
my $clientmodule = $hash->{$client}; my $clientmodule = $hash->{$client};
my $clienthash = $defs{$clientmodule}; my $clienthash = $defs{$clientmodule};
my $maxvol = Snapcast_getVolumeConstraint($clienthash); my $maxvol = getVolumeConstraint($clienthash) // 100;
if ( defined $clientmodule ) { if ( defined $clientmodule ) {
readingsBeginUpdate($clienthash); readingsBeginUpdate($clienthash);
@ -276,30 +365,23 @@ sub Snapcast_Read {
#Log3 $name,2, "group: $group "; #Log3 $name,2, "group: $group ";
for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) { for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) {
$client = $hash->{STATUS}->{clients}->{$i}->{id}; $client = $hash->{STATUS}->{clients}->{$i}->{id};
my $client_group = ReadingsVal( $hash->{NAME}, "clients_${client}_group", '' ); my $client_group = ReadingsVal( $name, "clients_${client}_group", '' );
next if $group ne $client_group;
#Log3 $name,2, "client_group: $client_group "; readingsBeginUpdate($hash);
my $clientmodule = $hash->{$client}; readingsBulkUpdateIfChanged( $hash, "clients_${client}_stream_id", $update->{result}->{stream_id} );
my $clienthash = $defs{$clientmodule}; readingsEndUpdate( $hash, 1 );
if ( $group eq $client_group ) { my $clienthash = $defs{$hash->{$client}} // next;
readingsBeginUpdate($hash); readingsBeginUpdate($clienthash);
readingsBulkUpdateIfChanged( $hash, "clients_${client}_stream_id", $update->{result}->{stream_id} ); readingsBulkUpdateIfChanged( $clienthash, 'stream_id', $update->{result}->{stream_id} );
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $clienthash, 1 );
if ( defined $clientmodule ) {
readingsBeginUpdate($clienthash);
readingsBulkUpdateIfChanged( $clienthash, 'stream_id', $update->{result}->{stream_id} );
readingsEndUpdate( $clienthash, 1 );
}
}
} }
} }
else { else {
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged( $hash, "clients_${client}_$key", $update->{result} ); readingsBulkUpdateIfChanged( $hash, "clients_${client}_$key", $update->{result} );
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
my $clientmodule = $hash->{$client}; my $clienthash = $defs{$hash->{$client}} // return;
my $clienthash = $defs{$clientmodule} // return;
return if !defined $clienthash;
readingsBeginUpdate($clienthash); readingsBeginUpdate($clienthash);
readingsBulkUpdateIfChanged( $clienthash, $key, $update->{result} ); readingsBulkUpdateIfChanged( $clienthash, $key, $update->{result} );
readingsEndUpdate( $clienthash, 1 ); readingsEndUpdate( $clienthash, 1 );
@ -309,14 +391,12 @@ sub Snapcast_Read {
delete $hash->{IDLIST}->{$id}; delete $hash->{IDLIST}->{$id};
return; return;
} }
elsif ( $update->{method} =~ /Client\.OnDelete/ ) {
my $s = $update->{params}->{data}; if ( $update->{method} eq 'Client.OnDelete' ) {
#fhem "deletereading $name clients.*"; #fhem "deletereading $name clients.*";
for my $reading ( for my $reading (
grep {m{\A(?:clients)}m} grep {m{\A(?:clients)}xms}
#keys %{$hash{READINGS}} ) {
keys %{ $hash->{READINGS} } keys %{ $hash->{READINGS} }
) )
{ {
@ -326,29 +406,35 @@ sub Snapcast_Read {
Snapcast_getStatus($hash); Snapcast_getStatus($hash);
return; return;
} }
elsif ( $update->{method} =~ /Client\./ ) {
my $c = $update->{params}->{data}; if ( $update->{method} =~ m{\AClient\.}x ) {
Snapcast_updateClient( $hash, $c, 0 ); updateClient( $hash, $s, 0 );
return; return;
} }
elsif ( $update->{method} =~ /Stream\./ ) {
my $s = $update->{params}->{data}; if ( $update->{method} =~ m{\A(?:Stream|Group)\.}x ) {
Snapcast_updateStream( $hash, $s, 0 ); updateStream( $hash, $s, 0 );
return; return;
} }
elsif ( $update->{method} =~ /Group\./ ) { # elsif ( $update->{method} =~ m{\AGroup\.}x ) {
my $s = $update->{params}->{data}; # updateStream( $hash, $s, 0 );
Snapcast_updateStream( $hash, $s, 0 ); # return;
# }
if ( $update->{method} eq 'Server.OnUpdate' ) {
my $serverupdate->{result} = $update->{params};
parseStatus( $hash, $serverupdate );
return; return;
} }
Log3( $name, 2, "unknown JSON, please contact module maintainer: $buf" ); Log3( $name, 2, "unknown JSON, please contact module maintainer: $buf" );
readingsSingleUpdate( $hash, 'lastError', "unknown JSON, please ontact module maintainer: $buf", 1 ); readingsSingleUpdate( $hash, 'lastError', "unknown JSON, please contact module maintainer: $buf", 1 );
return 'unknown JSON received'; return 'unknown JSON received';
} }
return; return;
} }
sub Snapcast_Ready { sub Ready {
my $hash = shift // return; my $hash = shift // return;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
return if AttrVal( $name, 'disable', 0 ); return if AttrVal( $name, 'disable', 0 );
@ -362,11 +448,11 @@ sub Snapcast_Ready {
readingsDelete( $hash, $reading ); readingsDelete( $hash, $reading );
} }
DevIo_OpenDev( $hash, 1, \&Snapcast_onConnect, sub() { } ); DevIo_OpenDev( $hash, 1, \&onConnect, sub() { } );
return; return;
} }
sub Snapcast_onConnect { sub onConnect {
my $hash = shift // return; my $hash = shift // return;
$hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() ); $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() );
$hash->{CONNECTS}++; $hash->{CONNECTS}++;
@ -375,7 +461,7 @@ sub Snapcast_onConnect {
return; return;
} }
sub Snapcast_updateClient { sub updateClient {
my $hash = shift // return; my $hash = shift // return;
my $c = shift // return; my $c = shift // return;
my $cnumber = shift // return; my $cnumber = shift // return;
@ -392,7 +478,8 @@ sub Snapcast_updateClient {
$hash->{STATUS}->{clients}->{$cnumber} = $c; $hash->{STATUS}->{clients}->{$cnumber} = $c;
my $id = $c->{id} ? $c->{id} : $c->{host}->{mac}; # protocol version 2 has no id, but just the MAC, newer versions will have an ID. my $id = $c->{id} ? $c->{id} : $c->{host}->{mac}; # protocol version 2 has no id, but just the MAC, newer versions will have an ID.
my $orig_id = $id; my $orig_id = $id;
$id =~ s/://g; $id =~ s{:}{}gx;
$id =~ s{[#]}{_}gx;
$hash->{STATUS}->{clients}->{$cnumber}->{id} = $id; $hash->{STATUS}->{clients}->{$cnumber}->{id} = $id;
$hash->{STATUS}->{clients}->{$cnumber}->{origid} = $orig_id; $hash->{STATUS}->{clients}->{$cnumber}->{origid} = $orig_id;
@ -411,6 +498,7 @@ sub Snapcast_updateClient {
readingsBulkUpdateIfChanged( $hash, "clients_${id}_group", $c->{config}->{group_id} ); readingsBulkUpdateIfChanged( $hash, "clients_${id}_group", $c->{config}->{group_id} );
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
return if !$hash->{$id};
my $clienthash = $defs{ $hash->{$id} } // return; my $clienthash = $defs{ $hash->{$id} } // return;
readingsBeginUpdate($clienthash); readingsBeginUpdate($clienthash);
@ -427,12 +515,45 @@ sub Snapcast_updateClient {
readingsBulkUpdateIfChanged( $clienthash, 'group', $c->{config}->{group_id} ); readingsBulkUpdateIfChanged( $clienthash, 'group', $c->{config}->{group_id} );
readingsEndUpdate( $clienthash, 1 ); readingsEndUpdate( $clienthash, 1 );
my $maxvol = Snapcast_getVolumeConstraint($clienthash); my $maxvol = getVolumeConstraint($clienthash) // 100;
Snapcast_setClient( $hash, $clienthash->{ID}, 'volume', $maxvol ) if $c->{config}->{volume}->{percent} > $maxvol; _setClient( $hash, $clienthash->{ID}, 'volume', $maxvol ) if $c->{config}->{volume}->{percent} > $maxvol;
return; return;
} }
sub Snapcast_updateStream { sub updateClientInGroup {
my $hash = shift // return;
my $c = shift // return;
my $cnumber = 1;
while ( defined $hash->{STATUS}->{clients}->{$cnumber} && $c->{params}->{id} ne $hash->{STATUS}->{clients}->{$cnumber}->{origid} ) {
$cnumber++;
}
if ( !defined $hash->{STATUS}->{clients}->{$cnumber} ) {
#Snapcast_getStatus($hash);
return;
}
return if !defined $c->{params};
my $id = $c->{params}->{id} // return; # recent version uses an ID.
$id =~ s{:}{}gx;
$id =~ s{[#]}{_}gx;
readingsBeginUpdate($hash);
readingsBulkUpdateIfChanged( $hash, "clients_${id}_volume", $c->{params}->{volume}->{percent} ) if defined $c->{params}->{volume}->{percent};
readingsBulkUpdateIfChanged( $hash, "clients_${id}_muted", $c->{params}->{volume}->{muted} ? 'true' : 'false' ) if defined $c->{params}->{volume}->{muted};
readingsEndUpdate( $hash, 1 );
return if !$hash->{$id};
my $clienthash = $defs{ $hash->{$id} } // return;
readingsBeginUpdate($clienthash);
readingsBulkUpdateIfChanged( $clienthash, 'volume', $c->{params}->{volume}->{percent} ) if defined $c->{params}->{volume}->{percent};
readingsBulkUpdateIfChanged( $clienthash, 'muted', $c->{params}->{volume}->{muted} ? 'true' : 'false' ) if defined $c->{params}->{volume}->{muted};
readingsEndUpdate( $clienthash, 1 );
return;
}
sub updateStream {
my $hash = shift // return; my $hash = shift // return;
my $s = shift // return; my $s = shift // return;
my $snumber = shift // return; my $snumber = shift // return;
@ -451,27 +572,27 @@ sub Snapcast_updateStream {
return; return;
} }
sub Snapcast_Client_Register_Server { sub Client_Register_Server {
my $hash = shift // return; my $hash = shift // return;
return if $hash->{MODE} ne 'client'; return if $hash->{MODE} ne 'client';
my $name = $hash->{NAME} // return; my $name = $hash->{NAME} // return;
my $server = $hash->{SERVER}; my $server = $hash->{SERVER};
if ( !defined $defs{$server} ) { if ( !defined $defs{$server} ) {
InternalTimer( gettimeofday() + 30, \&Snapcast_Client_Register_Server, $hash, 1 ); # if server does not exists maybe it got deleted, recheck every 30 seconds if it reappears InternalTimer( gettimeofday() + 30, \&Client_Register_Server, $hash, 1 ); # if server does not exists maybe it got deleted, recheck every 30 seconds if it reappears
return; return;
} }
$server = $defs{$server}; # get the server hash $server = $defs{$server}; # get the server hash
$server->{ $hash->{ID} } = $name; $server->{ $hash->{ID} } = $name;
Snapcast_getStatus($server); Snapcast_getStatus($server);
return; return;
} }
sub Snapcast_Client_Unregister_Server { sub Client_Unregister_Server {
my $hash = shift // return; my $hash = shift // return;
return if $hash->{MODE} ne 'client'; return if $hash->{MODE} ne 'client';
my $name = $hash->{NAME}; my $name = $hash->{NAME};
my $server = $hash->{SERVER}; my $server = $hash->{SERVER};
$server = $defs{$server} // return; # get the server hash $server = $defs{$server} // return; # get the server hash
readingsSingleUpdate( $server, "clients_$hash->{ID}_module", $name, 1 ); readingsSingleUpdate( $server, "clients_$hash->{ID}_module", $name, 1 );
delete $server->{ $hash->{ID} }; delete $server->{ $hash->{ID} };
return; return;
@ -482,7 +603,7 @@ sub Snapcast_getStatus {
return Snapcast_Do( $hash, 'Server.GetStatus', '' ); return Snapcast_Do( $hash, 'Server.GetStatus', '' );
} }
sub Snapcast_parseStatus { sub parseStatus {
my $hash = shift // return; my $hash = shift // return;
my $status = shift // return; my $status = shift // return;
my $streams = $status->{result}->{server}->{streams}; my $streams = $status->{result}->{server}->{streams};
@ -505,7 +626,7 @@ sub Snapcast_parseStatus {
for my $c (@clients) { for my $c (@clients) {
$c->{config}->{stream_id} = $groupstream; # insert "stream" field for every client $c->{config}->{stream_id} = $groupstream; # insert "stream" field for every client
$c->{config}->{group_id} = $groupid; # insert "group_id" field for every client $c->{config}->{group_id} = $groupid; # insert "group_id" field for every client
Snapcast_updateClient( $hash, $c, $cnumber ); updateClient( $hash, $c, $cnumber );
$cnumber++; $cnumber++;
} }
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
@ -518,7 +639,7 @@ sub Snapcast_parseStatus {
my @streams = @{$streams}; my @streams = @{$streams};
my $snumber = 1; my $snumber = 1;
for my $s (@streams) { for my $s (@streams) {
Snapcast_updateStream( $hash, $s, $snumber ); updateStream( $hash, $s, $snumber );
$snumber++; $snumber++;
} }
readingsBeginUpdate($hash); readingsBeginUpdate($hash);
@ -526,19 +647,18 @@ sub Snapcast_parseStatus {
readingsEndUpdate( $hash, 1 ); readingsEndUpdate( $hash, 1 );
} }
#readingsEndUpdate($hash,1) if defined $groups || defined $streams;
InternalTimer( gettimeofday() + 300, \&Snapcast_getStatus, $hash, 1 ); # every 5 Minutes, get the full update, also to apply changed vol constraints. InternalTimer( gettimeofday() + 300, \&Snapcast_getStatus, $hash, 1 ); # every 5 Minutes, get the full update, also to apply changed vol constraints.
return; return;
} }
sub Snapcast_setClient { sub _setClient {
my ( $hash, $id, $param, $value ) = @_; my ( $hash, $id, $param, $value ) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
Log3( $name, 4, "SNAP setClient: $hash->{NAME}, id $id, param $param, val $value" ); Log3( $name, 4, "SNAP setClient: $hash->{NAME}, id $id, param $param, val $value" );
my $cnumber = ReadingsVal( $name, "clients_${id}_nr", undef ) // return; my $cnumber = ReadingsVal( $name, "clients_${id}_nr", undef ) // return;
my $method = $Snapcast_clientmethods{$param} // return; my $method = $_clientmethods{$param} // return;
my $paramset->{id} = Snapcast_getId( $hash, $id ); my $paramset->{id} = _getId( $hash, $id );
Log3( $name, 4, "SNAP setClient still there: $hash->{NAME}, paramsetid $paramset->{id}" ); Log3( $name, 4, "SNAP setClient still there: $hash->{NAME}, paramsetid $paramset->{id}" );
if ( $param eq 'volumeConstraint' ) { if ( $param eq 'volumeConstraint' ) {
@ -553,8 +673,8 @@ sub Snapcast_setClient {
#$match = '_global_'; #$match = '_global_';
#} #}
for ( my $i = 0; $i < @values; $i += 2 ) { for ( my $i = 0; $i < @values; $i += 2 ) {
return 'wrong timeformat 00:00 - 24:00 for time/volume pair' if $values[$i] !~ /^(([0-1]?[0-9]|2[0-3]):[0-5][0-9])|24:00$/; return 'wrong timeformat 00:00 - 24:00 for time/volume pair' if $values[$i] !~ m{^(?:(?:[0-1]?[0-9]|2[0-3]):[0-5][0-9])|24:00$}x;
return 'wrong volumeformat 0 - 100 for time/volume pair' if $values[ $i + 1 ] !~ /^(0?[0-9]?[0-9]|100)$/; return 'wrong volumeformat 0 - 100 for time/volume pair' if $values[ $i + 1 ] !~ m{^(?:0?[0-9]?[0-9]|100)$}x;
} }
return; return;
} }
@ -564,7 +684,7 @@ sub Snapcast_setClient {
$param = 'stream_id'; $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 $totalstreams = ReadingsVal( $name, 'streams', 0 );
my $currentstream = Snapcast_getStreamNumber( $hash, ReadingsVal( $name, "clients_${id}_stream_id", '' ) ); my $currentstream = _getStreamNumber( $hash, ReadingsVal( $name, "clients_${id}_stream_id", '' ) );
my $newstream = $currentstream + 1; my $newstream = $currentstream + 1;
$newstream = 1 if $newstream > $totalstreams; #unless $newstream <= $totalstreams; $newstream = 1 if $newstream > $totalstreams; #unless $newstream <= $totalstreams;
$value = ReadingsVal( $name, "streams_" . $newstream . "_id", '' ); $value = ReadingsVal( $name, "streams_" . $newstream . "_id", '' );
@ -577,18 +697,16 @@ sub Snapcast_setClient {
if ( $param eq 'volume' ) { if ( $param eq 'volume' ) {
#return undef unless defined($currentVol);
return if !$value; return if !$value;
# check if volume was given as increment or decrement, then find out current volume and calculate new volume # check if volume was given as increment or decrement, then find out current volume and calculate new volume
if ( $value =~ /^([\+\-])(\d{1,2})$/ ) { if ( $value =~ m{\A([+-])(\d{1,2})\z}x ) {
my $direction = $1; my $direction = $1;
my $amount = $2; my $amount = $2;
#$value = eval($currentVol. $direction. $amount); #$value = eval($currentVol. $direction. $amount);
$value = eval { $currentVol . $direction . $amount }; $value = eval { $currentVol . $direction . $amount };
$value = 100 if $value > 100; $value = max( 0, min( 100, $value ) );
$value = 0 if $value < 0;
} }
# if volume is given with up or down argument, then increase or decrease according to volumeStepSize # if volume is given with up or down argument, then increase or decrease according to volumeStepSize
@ -600,8 +718,7 @@ sub Snapcast_setClient {
else { else {
$value = $currentVol - $step; $value = $currentVol - $step;
} }
$value = 100 if $value > 100; $value = max( 0, min( 100, $value ) );
$value = 0 if $value < 0;
$muteState = 'false' if $value > 0 && ( $muteState eq 'true' || $muteState == 1 ); $muteState = 'false' if $value > 0 && ( $muteState eq 'true' || $muteState == 1 );
} }
$volumeobject->{percent} = $value + 0; $volumeobject->{percent} = $value + 0;
@ -637,8 +754,9 @@ sub Snapcast_Do {
my $method = shift // return; my $method = shift // return;
my $param = shift // ''; my $param = shift // '';
my $payload = Snapcast_Encode( $hash, $method, $param ); my $payload = Snapcast_Encode( $hash, $method, $param );
$payload .= "\r\n";
#Log3($hash,5,"SNAP: Do $payload"); #Log3($hash,3,"SNAP: Do $payload");
return DevIo_SimpleWrite( $hash, $payload, 2 ); return DevIo_SimpleWrite( $hash, $payload, 2 );
} }
@ -658,13 +776,12 @@ sub Snapcast_Encode {
$request->{params} = $param if $param ne ''; $request->{params} = $param if $param ne '';
$hash->{IDLIST}->{ $request->{id} } = $request; $hash->{IDLIST}->{ $request->{id} } = $request;
$request->{id} = $request->{id} + 0; $request->{id} = $request->{id} + 0;
$json = encode_json($request) . "\r\n"; $json = JSON->new->encode($request);
$json =~ s/\"true\"/true/; # Snapcast needs bool values without "" but encode_json does not do this $json =~ s{("(true|false|null)")}{$2}gxms;
$json =~ s/\"false\"/false/;
return $json; return $json;
} }
sub Snapcast_getStreamNumber { sub _getStreamNumber {
my $hash = shift // return; my $hash = shift // return;
my $id = shift // return; my $id = shift // return;
my $name = $hash->{NAME} // return; my $name = $hash->{NAME} // return;
@ -674,21 +791,29 @@ sub Snapcast_getStreamNumber {
return; return;
} }
sub Snapcast_getId { sub _getId {
my $hash = shift // return; my $hash = shift // return;
my $client = shift // return; my $client = shift // return;
my $name = $hash->{NAME} // return; my $name = $hash->{NAME} // return;
if ( $client =~ m/^([0-9a-f]{12}(\#*\d*|$))$/i ) { # client is ID
# client is ID
if ( $client =~ m/^([0-9a-f]{12}([#_]*\d*|$))$/i ) {
for my $i ( 1 .. ReadingsVal( $name, 'streams', 1 ) ) { for my $i ( 1 .. ReadingsVal( $name, 'streams', 1 ) ) {
return $hash->{STATUS}->{clients}->{$i}->{origid} return $hash->{STATUS}->{clients}->{$i}->{origid}
if $client eq $hash->{STATUS}->{clients}->{$i}->{id}; if $client eq $hash->{STATUS}->{clients}->{$i}->{id};
} }
} }
# client is provided as device name?
if ( InternalVal($client,'TYPE','') eq 'Snapcast') {
my $def = InternalVal($client,'ID','');
return ReadingsVal($name,"clients_${def}_origid",undef);
}
return 'unknown client'; return 'unknown client';
} }
sub Snapcast_getVolumeConstraint { sub getVolumeConstraint {
my $hash = shift // return; my $hash = shift // return;
my $client = shift // return; my $client = shift // return;
@ -721,14 +846,16 @@ sub Snapcast_getVolumeConstraint {
return $value; # der aktuelle Auto-Wert wird zurückgegeben return $value; # der aktuelle Auto-Wert wird zurückgegeben
} }
1; 1;
__END__ __END__
=pod =pod
=encoding utf8
=item device
=item summary control and monitor Snapcast Servers and Clients =item summary control and monitor Snapcast Servers and Clients
=item summary_DE steuert und überwacht Snapcast Servers und Clients =item summary_DE Steuert und überwacht Snapcast Server und Clients
=begin html =begin html