From 4af598d1d589455d1959d3de25ad8ae69f58da67 Mon Sep 17 00:00:00 2001 From: Beta-User <> Date: Sat, 9 Jul 2022 12:04:51 +0000 Subject: [PATCH] 96_Snapcast: PBP code review (Part I) git-svn-id: https://svn.fhem.de/fhem/trunk@26203 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/CHANGED | 1 + fhem/FHEM/96_Snapcast.pm | 1274 +++++++++++---------- fhem/FHEM/lib/AttrTemplate/mqtt2.template | 5 +- 3 files changed, 650 insertions(+), 630 deletions(-) diff --git a/fhem/CHANGED b/fhem/CHANGED index f4107483b..93e17c3bc 100644 --- a/fhem/CHANGED +++ b/fhem/CHANGED @@ -1,5 +1,6 @@ # Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Do not insert empty lines here, update check depends on it. + - change: 96_Snapcast: PBP code restructured (part I) - bugfix: 49_SSCam: fix noQuotesForSID using in streaming devices type mjpeg - feature: 74_GardenaSmartDevice: add lona readings, [fix] - setter procedure - feature: 14_CUL_TCM97001: diff --git a/fhem/FHEM/96_Snapcast.pm b/fhem/FHEM/96_Snapcast.pm index 7be67f539..4794f33af 100755 --- a/fhem/FHEM/96_Snapcast.pm +++ b/fhem/FHEM/96_Snapcast.pm @@ -2,13 +2,7 @@ # # $Id$ # -# Maintainer: Sebatian Stuecker / FHEM Forum: unimatrix / Github: unimatrix27 -# -# FHEM Forum : https://forum.fhem.de/index.php/topic,62389.0.html -# -# Github: https://github.com/unimatrix27/fhemmodules/blob/master/96_Snapcast.pm -# -# Feedback bitte nur ins FHEM Forum, Bugs oder Pull Request bitte direkt auf Github. +# Originally initiated by Sebatian Stuecker / FHEM Forum: unimatrix # # This code is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -27,683 +21,707 @@ # Future developments beyond this revision are not necessarily supported. # 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. -# It is necessary to have JSON module installed. If not, the module will detect this and put a message in the log file. package main; use strict; use warnings; use Scalar::Util qw(looks_like_number); +use DevIo; +use JSON; my %Snapcast_sets = ( - "update" => 0, - "volume" => 2, - "stream" => 2, - "name" => 2, - "mute" => 2, - "latency" => 2, - "group" => 2, + update => 0, + volume => 2, + stream => 2, + name => 2, + mute => 2, + latency => 2, + group => 2, ); my %Snapcast_client_sets = ( - "volume" => 1, - "stream" => 1, - "name" => 1, - "mute" => 0, - "latency" => 1, - "group" => 1, + volume => 1, + stream => 1, + name => 1, + mute => 0, + latency => 1, + group => 1, ); my %Snapcast_clientmethods = ( - "name" => "Client.SetName", - "volume" => "Client.SetVolume", - "mute" => "Client.SetVolume", - "stream" => "Group.SetStream", - "latency" => "Client.SetLatency" + name => 'Client.SetName', + volume => 'Client.SetVolume', + mute => 'Client.SetVolume', + stream => 'Group.SetStream', + latency => 'Client.SetLatency' ); - -sub Snapcast_Initialize($) { - my ($hash) = @_; - use DevIo; - $hash->{DefFn} = 'Snapcast_Define'; - $hash->{UndefFn} = 'Snapcast_Undef'; - $hash->{SetFn} = 'Snapcast_Set'; - $hash->{GetFn} = 'Snapcast_Get'; - $hash->{WriteFn} = 'Snapcast_Write'; - $hash->{ReadyFn} = 'Snapcast_Ready'; - $hash->{AttrFn} = 'Snapcast_Attr'; - $hash->{ReadFn} = 'Snapcast_Read'; - $hash->{AttrList} = - "streamnext:all,playing constraintDummy constraints volumeStepSize volumeStepSizeSmall volumeStepSizeThreshold " . $readingFnAttributes; +sub Snapcast_Initialize { + my $hash = shift // return; + $hash->{DefFn} = \&Snapcast_Define; + $hash->{UndefFn} = \&Snapcast_Undef; + $hash->{SetFn} = \&Snapcast_Set; + $hash->{GetFn} = \&Snapcast_Get; + $hash->{WriteFn} = \&Snapcast_Write; + $hash->{ReadyFn} = \&Snapcast_Ready; + $hash->{AttrFn} = \&Snapcast_Attr; + $hash->{ReadFn} = \&Snapcast_Read; + $hash->{AttrList} = "streamnext:all,playing constraintDummy constraints disable:1 volumeStepSize volumeStepSizeSmall volumeStepSizeThreshold $readingFnAttributes"; + return; } -sub Snapcast_Define($$) { - my ($hash, $def) = @_; - my @a = split('[ \t]+', $def); - return "ERROR: perl module JSON is not installed" if (Snapcast_isPmInstalled($hash,"JSON")); - my $name= $hash->{name} = $a[0]; - if(defined($a[2]) && $a[2] eq "client"){ - return "Usage: define Snapcast client " unless (defined($a[3]) && defined($a[4])); - return "Server $a[3] not defined" unless defined ($defs{$a[3]}); - $hash->{MODE} = "client"; - $hash->{SERVER} = $a[3]; - $hash->{ID} = $a[4]; - readingsSingleUpdate($hash,"state","defined",1); +sub Snapcast_Define { + my $hash = shift // return; + my $def = shift // return; + my @arr = split m{\s+}xms, $def; + my $name = shift @arr; + if ( defined( $arr[1] ) && $arr[1] eq 'client' ) { + return 'Usage: define Snapcast client ' if !defined $arr[2] || !defined $arr[3]; + return "Server $arr[2] not defined" if !defined $defs{ $arr[2] }; + $hash->{MODE} = 'client'; + $hash->{SERVER} = $arr[2]; + $hash->{ID} = $arr[3]; + readingsSingleUpdate( $hash, 'state', 'defined', 1 ); RemoveInternalTimer($hash); DevIo_CloseDev($hash); - $attr{$name}{volumeStepSize} = '5' unless (exists($attr{$name}{volumeStepSize})); - $attr{$name}{volumeStepSizeSmall} = '1' unless (exists($attr{$name}{volumeStepSizeSmall})); - $attr{$name}{volumeStepSizeThreshold} = '5' unless (exists($attr{$name}{volumeStepSizeThreshold})); return Snapcast_Client_Register_Server($hash); } - $hash->{ip} = (defined($a[2])) ? $a[2] : "localhost"; - $hash->{port} = (defined($a[3])) ? $a[3] : "1705"; - $hash->{MODE} = "server"; - readingsSingleUpdate($hash,"state","defined",1); + $hash->{ip} = $arr[1] // 'localhost'; + $hash->{port} = $arr[2] // '1705'; + $hash->{MODE} = 'server'; + readingsSingleUpdate( $hash, 'state', 'defined', 1 ); RemoveInternalTimer($hash); DevIo_CloseDev($hash); - $hash->{DeviceName} = $hash->{ip}.":".$hash->{port}; - $attr{$name}{volumeStepSize} = '5' unless (exists($attr{$name}{volumeStepSize})); - delete($hash->{"IDLIST"}); + $hash->{DeviceName} = "$hash->{ip}:$hash->{port}"; + delete( $hash->{IDLIST} ); Snapcast_Connect($hash); - return undef; + return; } -sub Snapcast_Connect($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - if (!$init_done){ - RemoveInternalTimer($hash); - InternalTimer(gettimeofday()+5,"Snapcast_Connect", $hash, 0); - return "init not done"; - }else{ - return DevIo_OpenDev($hash,0,"Snapcast_onConnect",); - } +sub Snapcast_Connect { + my $hash = shift // return; + if ( !$init_done ) { + RemoveInternalTimer($hash); + InternalTimer( gettimeofday() + 5, \&Snapcast_Connect, $hash, 0 ); + return; # "init not done"; + } + return DevIo_OpenDev( $hash, 0, \&Snapcast_onConnect, ); } -sub Snapcast_Attr($$){ - my ($cmd, $name, $attr, $value) = @_; - my $hash = $defs{$name}; - if ($cmd eq "set"){ - if($attr eq "streamnext"){ - return "streamnext needs to be either all or playing" unless $value=~/(all)|(playing)/; +sub Snapcast_Attr { + my $cmd = shift; + my $name = shift; + my $attr = shift // return; + my $value = shift; + my $hash = $defs{$name} // return; + + if ( $cmd eq 'set' ) { + if ( $attr eq 'streamnext' ) { + return 'streamnext needs to be either all or playing' if $value !~ m{\A(?:all|playing)\z}x; + } + if ( $attr eq 'volumeStepSize' || $attr eq 'volumeStepSizeSmall' || $attr eq 'volumeStepSizeThreshold' ) { + return "$attr needs to be a number between 1 and 100" if !looks_like_number($value) || $value < 1 || $value > 100; + } } - if($attr eq "volumeStepSize"){ - return "volumeStepSize needs to be a number between 1 and 100" unless $value>0 && $value <=100; - } - if($attr eq "volumeStepSizeSmall"){ - return "volumeStepSizeSmall needs to be a number between 1 and 100" unless $value>0 && $value <=100; - } - if($attr eq "volumeStepSizeThreshold"){ - return "volumeStepSizeThreshold needs to be a number between 0 and 100" unless $value>=0 && $value <=100; - } - } - return undef; + return; } -sub Snapcast_Undef($$) { - my ($hash, $arg) = @_; +sub Snapcast_Undef { + my $hash = shift // return; RemoveInternalTimer($hash); DevIo_CloseDev($hash); - if($hash->{MODE} eq "client"){ - Snapcast_Client_Unregister_Server($hash); - } - return undef; -} - - -sub Snapcast_Get($@) { - return "get is not supported by this module"; -} - -sub Snapcast_Set($@) { - my ($hash, @param) = @_; - return '"set Snapcast" needs at least one argument' if (int(@param) < 2); - my $name = shift @param; - my $opt = shift @param; - my $value = join(" ", @param); -# my $clientmod; - my %sets = ($hash->{MODE} eq "client") ? %Snapcast_client_sets : %Snapcast_sets; - if(!defined($sets{$opt})) { - my @cList = keys %sets; - return "Unknown argument $opt, choose one of " . join(" ", @cList); - } - if(@param < $sets{$opt}){ - return "$opt requires at least ".$sets{$opt}." arguments"; - } - if($opt eq "update"){ - Snapcast_getStatus($hash); - return undef; - } - if(defined($Snapcast_clientmethods{$opt})){ - my $client; - if($hash->{MODE} eq "client"){ - my $clientmod=$hash; - $client=$hash->{NAME}; - $hash=$hash->{SERVER}; - $hash=$defs{$hash}; - $client = $clientmod->{ID}; - return "Cannot find Server hash" unless defined ($hash); - }else{ - $client = shift @param; - } - $value = join(" ", @param); - return "client not found, use unique name, IP, or MAC as client identifier" unless defined($client); - if($client eq "all"){ - for(my $i=1;$i<=ReadingsVal($name,"clients",0);$i++){ - my $client = $hash->{STATUS}->{clients}->{"$i"}->{host}->{mac}; - $client=~s/\://g; - my $res = Snapcast_setClient($hash,$client,$opt,$value); - readingsSingleUpdate($hash,"lastError",$res,1) if defined ($res); - } - return undef; - } - my $res = Snapcast_setClient($hash,$client,$opt,$value); - readingsSingleUpdate($hash,"lastError",$res,1) if defined ($res); - return undef; - } - return "$opt not implemented"; -} - -sub Snapcast_Read($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - my $buf; - $buf = DevIo_SimpleRead($hash); - return "" if ( !defined($buf) ); - $buf = $hash->{PARTIAL} . $buf; - $buf =~ s/\r//g; - my $lastchr = substr( $buf, -1, 1 ); - if ( $lastchr ne "\n") { - $hash->{PARTIAL} = $buf; - Log3( $hash, 5, "snap: partial command received" ); - return; - }else { - $hash->{PARTIAL} = ""; - } - - ############################### - # Log3 $name,2, "Buffer: $buf"; - ############################### - - my @lines = split( /\n/, $buf ); - foreach my $line (@lines) { - # Hier die Results parsen - my $decoded_json; - eval { - $decoded_json = decode_json($line); - 1; - } or do { - # Decode JSON died, probably because of incorrect JSON from Snapcast. - Log3 $name,2, "Invalid Response from Snapcast,ignoring result: $line"; - readingsSingleUpdate($hash,"lastError","Invalid JSON: $buf",1); - return undef; - }; - my $update=$decoded_json; - if(defined ($hash->{"IDLIST"}->{$update->{id}})){ - my $id=$update->{id}; - #Log3 $name,2, "id: $id "; - if($hash->{"IDLIST"}->{$id}->{method} eq 'Server.GetStatus'){ - delete $hash->{"IDLIST"}->{$id}; - return Snapcast_parseStatus($hash,$update); - } - if($hash->{"IDLIST"}->{$id}->{method} eq 'Server.DeleteClient'){ - delete $hash->{"IDLIST"}->{$id}; - return undef; - } - while ( my ($key, $value) = each %Snapcast_clientmethods){ - 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}; - - $client=~s/\://g; - Log3 $name,2, "client: $client "; - Log3 $name,2, "key: $key "; - Log3 $name,2, "value: $value "; - if($key eq "volume"){ - my $temp_percent = $update->{result}->{volume}->{percent}; - #Log3 $name,2, "percent: $temp_percent "; - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"clients_".$client."_muted",$update->{result}->{volume}->{muted} ); - readingsBulkUpdateIfChanged($hash,"clients_".$client."_volume",$update->{result}->{volume}->{percent} ); - readingsEndUpdate($hash,1); - my $clientmodule = $hash->{$client}; - my $clienthash=$defs{$clientmodule}; - my $maxvol = Snapcast_getVolumeConstraint($clienthash); - if (defined $clientmodule) { - readingsBeginUpdate($clienthash); - readingsBulkUpdateIfChanged($clienthash,"muted",$update->{result}->{volume}->{muted} ); - readingsBulkUpdateIfChanged($clienthash,"volume",$update->{result}->{volume}->{percent} ); - readingsEndUpdate($clienthash,1); - } - } - elsif($key eq "stream"){ - #Log3 $name,2, "key: $key "; - my $group = $hash->{"IDLIST"}->{$id}->{params}->{id}; - #Log3 $name,2, "group: $group "; - for(my $i=1;$i<=ReadingsVal($name,"clients",1);$i++){ - $client = $hash->{STATUS}->{clients}->{"$i"}->{id}; - my $client_group = ReadingsVal($hash->{NAME},"clients_".$client."_group",""); - #Log3 $name,2, "client_group: $client_group "; - my $clientmodule = $hash->{$client}; - my $clienthash=$defs{$clientmodule}; - if ($group eq $client_group) { - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"clients_".$client."_stream_id",$update->{result}->{stream_id} ); - readingsEndUpdate($hash,1); - if (defined $clientmodule) { - readingsBeginUpdate($clienthash); - readingsBulkUpdateIfChanged($clienthash,"stream_id",$update->{result}->{stream_id} ); - readingsEndUpdate($clienthash,1); - } - } - } - } - else{ - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"clients_".$client."_".$key,$update->{result}); - readingsEndUpdate($hash,1); - my $clientmodule = $hash->{$client}; - my $clienthash=$defs{$clientmodule}; - return undef unless defined ($clienthash); - readingsBeginUpdate($clienthash); - readingsBulkUpdateIfChanged($clienthash,$key,$update->{result} ); - readingsEndUpdate($clienthash,1); - } - } - } - delete $hash->{"IDLIST"}->{$id}; - return undef; - } - elsif($update->{method}=~/Client\.OnDelete/){ - my $s=$update->{params}->{data}; - fhem "deletereading $name clients.*"; - Snapcast_getStatus($hash); - return undef; - } - elsif($update->{method}=~/Client\./){ - my $c=$update->{params}->{data}; - Snapcast_updateClient($hash,$c,0); - return undef; - } - elsif($update->{method}=~/Stream\./){ - my $s=$update->{params}->{data}; - Snapcast_updateStream($hash,$s,0); - return undef; - } - elsif($update->{method}=~/Group\./){ - my $s=$update->{params}->{data}; - Snapcast_updateStream($hash,$s,0); - return undef; - } - Log3 $name,2,"unknown JSON, please ontact module maintainer: $buf"; - readingsSingleUpdate($hash,"lastError","unknown JSON, please ontact module maintainer: $buf",1); - return "unknown JSON received" - } -} - - -sub Snapcast_Ready($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - if (AttrVal($hash->{NAME}, 'disable', 0)) { + Snapcast_Client_Unregister_Server($hash) if $hash->{MODE} eq 'client'; return; - } - if ( ReadingsVal( $name, "state", "disconnected" ) eq "disconnected" ) { - fhem "deletereading ".$name." streams.*"; - fhem "deletereading ".$name." clients.*"; - DevIo_OpenDev($hash, 1,"Snapcast_onConnect"); +} + +sub Snapcast_Get { + return 'get is not supported by this module'; +} + +sub Snapcast_Set { + my ( $hash, @param ) = @_; + return '"set Snapcast" needs at least one argument' if int @param < 2; + my $name = shift @param; + my $opt = shift @param; + + # my $clientmod; + my %snap_sets = $hash->{MODE} eq 'client' ? %Snapcast_client_sets : %Snapcast_sets; + if ( !defined $snap_sets{$opt} ) { + my @cList = keys %snap_sets; + return "Unknown argument $opt, choose one of " . join( q{ }, @cList ); + } + if ( @param < $snap_sets{$opt} ) { + return "$opt requires at least $snap_sets{$opt} arguments"; + } + if ( $opt eq 'update' ) { + Snapcast_getStatus($hash); return; } - return undef; -} - -sub Snapcast_onConnect($) -{ - my ($hash) = @_; - my $name = $hash->{NAME}; - $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() ); - $hash->{CONNECTS}++; - $hash->{helper}{PARTIAL} = ""; - Snapcast_getStatus($hash); - return undef; - } - -sub Snapcast_updateClient($$$){ - my ($hash,$c,$cnumber) = @_; - my $name = $hash->{NAME}; - if($cnumber==0){ - $cnumber++; - while(defined($hash->{STATUS}->{clients}->{"$cnumber"}) && $c->{host}->{mac} ne $hash->{STATUS}->{clients}->{"$cnumber"}->{host}->{mac}){$cnumber++} - if (not defined ($hash->{STATUS}->{clients}->{"$cnumber"})) { - Snapcast_getStatus($hash); - return undef; - } - } - $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 $orig_id = $id; - $id =~ s/://g; - $hash->{STATUS}->{clients}->{"$cnumber"}->{id}=$id; - $hash->{STATUS}->{clients}->{"$cnumber"}->{origid}=$orig_id; - - my $clientmodule = $hash->{$id}; - my $clienthash=$defs{$clientmodule}; - - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_online",$c->{connected} ? 'true' : 'false' ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_name",$c->{config}->{name} ? $c->{config}->{name} : $c->{host}->{name} ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_latency",$c->{config}->{latency} ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_stream_id",$c->{config}->{stream_id} ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_volume",$c->{config}->{volume}->{percent} ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_muted",$c->{config}->{volume}->{muted} ? 'true' : 'false' ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_ip",$c->{host}->{ip} ); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_mac",$c->{host}->{mac}); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_id",$id); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_origid",$orig_id); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_nr",$cnumber); - readingsBulkUpdateIfChanged($hash,"clients_".$id."_group",$c->{config}->{group_id}); - readingsEndUpdate($hash,1); - - return undef unless defined ($clienthash); - - readingsBeginUpdate($clienthash); - readingsBulkUpdateIfChanged($clienthash,"online",$c->{connected} ? 'true' : 'false' ); - readingsBulkUpdateIfChanged($clienthash,"name",$c->{config}->{name} ? $c->{config}->{name} : $c->{host}->{name} ); - readingsBulkUpdateIfChanged($clienthash,"latency",$c->{config}->{latency} ); - readingsBulkUpdateIfChanged($clienthash,"stream_id",$c->{config}->{stream_id} ); - readingsBulkUpdateIfChanged($clienthash,"volume",$c->{config}->{volume}->{percent} ); - readingsBulkUpdateIfChanged($clienthash,"muted",$c->{config}->{volume}->{muted} ? 'true' : 'false' ); - readingsBulkUpdateIfChanged($clienthash,"ip",$c->{host}->{ip} ); - readingsBulkUpdateIfChanged($clienthash,"mac",$c->{host}->{mac}); - readingsBulkUpdateIfChanged($clienthash,"id",$id); - readingsBulkUpdateIfChanged($clienthash,"origid",$orig_id); - readingsBulkUpdateIfChanged($clienthash,"group",$c->{config}->{group_id}); - readingsEndUpdate($clienthash,1); - my $maxvol = Snapcast_getVolumeConstraint($clienthash); - if($c->{config}->{volume}->{percent} > $maxvol){ - Snapcast_setClient($hash,$clienthash->{ID},"volume",$maxvol); - } - return undef; -} - -sub Snapcast_updateStream($$$){ - my ($hash,$s,$snumber) = @_; - my $name = $hash->{NAME}; - if($snumber==0){ - $snumber++; - while(defined($hash->{STATUS}->{streams}->{"$snumber"}) && $s->{id} ne $hash->{STATUS}->{streams}->{"$snumber"}->{id}){$snumber++} - if (not defined ($hash->{STATUS}->{streams}->{"$snumber"})){ return undef;} - } - $hash->{STATUS}->{streams}->{"$snumber"}=$s; - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"streams_".$snumber."_id",$s->{id} ); - readingsBulkUpdateIfChanged($hash,"streams_".$snumber."_status",$s->{status} ); - readingsEndUpdate($hash,1); -} - -sub Snapcast_Client_Register_Server($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - return undef unless $hash->{MODE} eq "client"; - my $server = $hash->{SERVER}; - if (not 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 - return undef; - } - my $id=$hash->{ID}; - $server = $defs{$server}; # get the server hash - return undef unless defined($server); - $server->{$id} = $name; - Snapcast_getStatus($server); - return undef; -} - -sub Snapcast_Client_Unregister_Server($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - return undef unless $hash->{MODE} eq "client"; - my $server = $hash->{SERVER}; - return undef if (not defined ($defs{$server})); - my $id=$hash->{ID}; - $server = $defs{$server}; # get the server hash - return undef unless defined($server); - readingsSingleUpdate($server,"clients_".$id."_module",$name,1 ); - delete($server->{$id}); - return undef; -} - -sub Snapcast_getStatus($){ - my ($hash) = @_; - my $name = $hash->{NAME}; - - return Snapcast_Do($hash,"Server.GetStatus",''); -} - -sub Snapcast_parseStatus($$){ - my ($hash,$status) = @_; - my $streams=$status->{result}->{server}->{streams}; - my $groups=$status->{result}->{server}->{groups}; - my $server=$status->{result}->{server}->{server}; - - - $hash->{STATUS}->{server}=$server; - if(defined ($groups)){ - my @groups=@{$groups}; - my $gnumber=1; - my $cnumber=1; - foreach my $g(@groups){ - my $groupstream=$g->{stream_id}; - my $groupid = $g->{id}; - my $clients=$g->{clients}; - if(defined ($clients)){ - my @clients=@{$clients}; - foreach my $c(@clients){ - $c->{config}->{stream_id} = $groupstream; # insert "stream" field for every client - $c->{config}->{group_id} = $groupid; # insert "group_id" field for every client - Snapcast_updateClient($hash,$c,$cnumber); - $cnumber++; + if ( defined $Snapcast_clientmethods{$opt} ) { + my $client; + Log3( $hash, 5, "snap: $opt command received" ); + if ( $hash->{MODE} eq 'client' ) { + my $clientmod = $hash; + $client = $hash->{NAME}; + $hash = $hash->{SERVER}; + $hash = $defs{$hash} // return 'Cannot find Server hash'; + $client = $clientmod->{ID}; } - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"clients",$cnumber-1 ); - readingsEndUpdate($hash,1); - } + else { + $client = shift @param; + } + Log3( $hash, 5, "Snap: $opt command received for $client" ); + + return 'client not found, use unique name, IP, or MAC as client identifier' if !defined $client; + + my $value = join q{ }, @param; + + if ( $client eq 'all' ) { + for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) { + my $sclient = $hash->{STATUS}->{clients}->{$i}->{host}->{mac}; + $sclient =~ s/\://g; + my $res = Snapcast_setClient( $hash, $sclient, $opt, $value ); + readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res; + } + return; + } + my $res = Snapcast_setClient( $hash, $client, $opt, $value ); + readingsSingleUpdate( $hash, 'lastError', $res, 1 ) if defined $res; + return; } - } - if(defined ($streams)){ - my @streams=@{$streams} unless not defined ($streams); - my $snumber=1; - foreach my $s(@streams){ - Snapcast_updateStream($hash,$s,$snumber); - $snumber++; - } - readingsBeginUpdate($hash); - readingsBulkUpdateIfChanged($hash,"streams",$snumber-1 ); - readingsEndUpdate($hash,1); - } - InternalTimer(gettimeofday() + 300, "Snapcast_getStatus", $hash, 1); # every 5 Minutes, get the full update, also to apply changed vol constraints. + return "$opt not implemented"; } -sub Snapcast_setClient($$$$){ - my ($hash,$id,$param,$value) = @_; - my $name = $hash->{NAME}; - my $method; - my $paramset; - my $cnumber = ReadingsVal($name,"clients_".$id."_nr",""); - return undef unless defined($cnumber); - $paramset->{id} = Snapcast_getId($hash,$id); - return undef unless defined($Snapcast_clientmethods{$param}); - $method=$Snapcast_clientmethods{$param}; - if($param eq "volumeConstraint"){ - my @values=split(/ /,$value); - my $match; - return "not enough parameters for volumeConstraint" unless @values>=2; - if(@values%2){ # there is a match argument given because number is uneven - $match=pop(@values); - } else { - $match="_global_"; - } - for(my $i=0;$i<@values;$i+=2){ - return "wrong timeformat 00:00 - 24:00 for time/volume pair" unless $values[$i]=~/^(([0-1]?[0-9]|2[0-3]):[0-5][0-9])|24:00$/; - return "wrong volumeformat 0 - 100 for time/volume pair" unless $values[$i+1]=~/^(0?[0-9]?[0-9]|100)$/; - } - return undef; - } - 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 - my $totalstreams=ReadingsVal($name,"streams",""); - my $currentstream = ReadingsVal($name,"clients_".$id."_stream_id",""); - $currentstream = Snapcast_getStreamNumber($hash,$currentstream); - my $newstream = $currentstream+1; - $newstream=1 unless $newstream <= $totalstreams; - $value=ReadingsVal($name,"streams_".$newstream."_id",""); - } - } +sub Snapcast_Read { + my $hash = shift // return; + my $name = $hash->{NAME}; + my $buf; + $buf = DevIo_SimpleRead($hash); + return '' if !defined $buf; + $buf = $hash->{PARTIAL} . $buf; + $buf =~ s/\r//g; + my $lastchr = substr( $buf, -1, 1 ); - if($param eq "volume"){ - my $currentVol = ReadingsVal($name,"clients_".$id."_volume",""); - my $muteState = ReadingsVal($name,"clients_".$id."_muted",""); - return undef unless defined($currentVol); + if ( $lastchr ne "\n" ) { + $hash->{PARTIAL} = $buf; + #Log3( $hash, 5, "snap: partial command received" ); + return; + } + $hash->{PARTIAL} = ''; - # check if volume was given as increment or decrement, then find out current volume and calculate new volume - if($value=~/^([\+\-])(\d{1,2})$/){ - my $direction = $1; - my $amount = $2; - $value = eval($currentVol. $direction. $amount); - $value = 100 if ($value >= 100); - $value = 0 if ($value <0); + ############################### + # Log3 $name,2, "Buffer: $buf"; + ############################### + + my @lines = split m{\n}x, $buf; + for my $line (@lines) { + + # Hier die Results parsen + my $decoded_json; + if (!eval { + $decoded_json = decode_json($line); + 1; + } + ) + { + # Decode JSON died, probably because of incorrect JSON from Snapcast. + Log3( $name, 1, "Invalid Response from Snapcast,ignoring result: $line" ); + readingsSingleUpdate( $hash, 'lastError', "Invalid JSON: $buf", 1 ); + return; + } + my $update = $decoded_json; + if ( defined $hash->{IDLIST} && defined $hash->{IDLIST}->{ $update->{id} } ) { + my $id = $update->{id}; + + #Log3 $name,2, "id: $id "; + if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.GetStatus' ) { + delete $hash->{IDLIST}->{$id}; + return Snapcast_parseStatus( $hash, $update ); + } + if ( $hash->{IDLIST}->{$id}->{method} eq 'Server.DeleteClient' ) { + delete $hash->{IDLIST}->{$id}; + return; + } + while ( my ( $key, $value ) = each %Snapcast_clientmethods ) { + 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}; + + $client =~ s/\://g; + Log3( $name, 5, "client: $client, key: $key, value: $value" ); + + if ( $key eq 'volume' ) { + my $temp_percent = $update->{result}->{volume}->{percent}; + + #Log3 $name,2, "percent: $temp_percent "; + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, "clients_${client}_muted", $update->{result}->{volume}->{muted} ); + readingsBulkUpdateIfChanged( $hash, "clients_${client}_volume", $update->{result}->{volume}->{percent} ); + readingsEndUpdate( $hash, 1 ); + my $clientmodule = $hash->{$client}; + my $clienthash = $defs{$clientmodule}; + my $maxvol = Snapcast_getVolumeConstraint($clienthash); + + if ( defined $clientmodule ) { + readingsBeginUpdate($clienthash); + readingsBulkUpdateIfChanged( $clienthash, 'muted', $update->{result}->{volume}->{muted} ); + readingsBulkUpdateIfChanged( $clienthash, 'volume', $update->{result}->{volume}->{percent} ); + readingsEndUpdate( $clienthash, 1 ); + } + } + elsif ( $key eq 'stream' ) { + + #Log3 $name,2, "key: $key "; + my $group = $hash->{IDLIST}->{$id}->{params}->{id}; + + #Log3 $name,2, "group: $group "; + for my $i ( 1 .. ReadingsVal( $name, 'clients', 0 ) ) { + $client = $hash->{STATUS}->{clients}->{$i}->{id}; + my $client_group = ReadingsVal( $hash->{NAME}, "clients_${client}_group", '' ); + + #Log3 $name,2, "client_group: $client_group "; + my $clientmodule = $hash->{$client}; + my $clienthash = $defs{$clientmodule}; + if ( $group eq $client_group ) { + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, "clients_${client}_stream_id", $update->{result}->{stream_id} ); + readingsEndUpdate( $hash, 1 ); + if ( defined $clientmodule ) { + readingsBeginUpdate($clienthash); + readingsBulkUpdateIfChanged( $clienthash, 'stream_id', $update->{result}->{stream_id} ); + readingsEndUpdate( $clienthash, 1 ); + } + } + } + } + else { + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, "clients_${client}_$key", $update->{result} ); + readingsEndUpdate( $hash, 1 ); + my $clientmodule = $hash->{$client}; + my $clienthash = $defs{$clientmodule} // return; + return if !defined $clienthash; + readingsBeginUpdate($clienthash); + readingsBulkUpdateIfChanged( $clienthash, $key, $update->{result} ); + readingsEndUpdate( $clienthash, 1 ); + } + } + } + delete $hash->{IDLIST}->{$id}; + return; + } + elsif ( $update->{method} =~ /Client\.OnDelete/ ) { + my $s = $update->{params}->{data}; + + #fhem "deletereading $name clients.*"; + for my $reading ( + grep {m{\A(?:clients)}m} + + #keys %{$hash{READINGS}} ) { + keys %{ $hash->{READINGS} } + ) + { + readingsDelete( $hash, $reading ); + } + + Snapcast_getStatus($hash); + return; + } + elsif ( $update->{method} =~ /Client\./ ) { + my $c = $update->{params}->{data}; + Snapcast_updateClient( $hash, $c, 0 ); + return; + } + elsif ( $update->{method} =~ /Stream\./ ) { + my $s = $update->{params}->{data}; + Snapcast_updateStream( $hash, $s, 0 ); + return; + } + elsif ( $update->{method} =~ /Group\./ ) { + my $s = $update->{params}->{data}; + Snapcast_updateStream( $hash, $s, 0 ); + return; + } + Log3( $name, 2, "unknown JSON, please contact module maintainer: $buf" ); + readingsSingleUpdate( $hash, 'lastError', "unknown JSON, please ontact module maintainer: $buf", 1 ); + return 'unknown JSON received'; } - # if volume is given with up or down argument, then increase or decrease according to volumeStepSize - if($value=~/^(up|down)$/){ - my $step = AttrVal($name,"volumeStepSizeThreshold",0) > $currentVol ? AttrVal($name,"volumeStepSizeSmall",3) : AttrVal($name,"volumeStepSize",7); - if ($value eq "up"){$value = $currentVol + $step;}else{$value = $currentVol - $step;} - $value = 100 if ($value >= 100); - $value = 0 if ($value <0); - $muteState = "false" if $value > 0 && ($muteState eq "true" || $muteState == 1); + return; +} + +sub Snapcast_Ready { + my $hash = shift // return; + my $name = $hash->{NAME}; + return if AttrVal( $name, 'disable', 0 ); + return if ReadingsVal( $name, 'state', 'disconnected' ) ne 'disconnected'; + + for my $reading ( + grep {m{\A(?:streams|clients)}xms} + keys %{ $hash->{READINGS} } + ) + { + readingsDelete( $hash, $reading ); } + + DevIo_OpenDev( $hash, 1, \&Snapcast_onConnect, sub() { } ); + return; +} + +sub Snapcast_onConnect { + my $hash = shift // return; + $hash->{LAST_CONNECT} = FmtDateTime( gettimeofday() ); + $hash->{CONNECTS}++; + $hash->{helper}{PARTIAL} = ''; + Snapcast_getStatus($hash); + return; +} + +sub Snapcast_updateClient { + my $hash = shift // return; + my $c = shift // return; + my $cnumber = shift // return; + if ( $cnumber == 0 ) { + $cnumber++; + while ( defined $hash->{STATUS}->{clients}->{$cnumber} && $c->{host}->{mac} ne $hash->{STATUS}->{clients}->{$cnumber}->{host}->{mac} ) { + $cnumber++; + } + if ( !defined $hash->{STATUS}->{clients}->{$cnumber} ) { + Snapcast_getStatus($hash); + return; + } + } + $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 $orig_id = $id; + $id =~ s/://g; + $hash->{STATUS}->{clients}->{$cnumber}->{id} = $id; + $hash->{STATUS}->{clients}->{$cnumber}->{origid} = $orig_id; + + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_online", $c->{connected} ? 'true' : 'false' ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_name", $c->{config}->{name} ? $c->{config}->{name} : $c->{host}->{name} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_latency", $c->{config}->{latency} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_stream_id", $c->{config}->{stream_id} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_volume", $c->{config}->{volume}->{percent} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_muted", $c->{config}->{volume}->{muted} ? 'true' : 'false' ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_ip", $c->{host}->{ip} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_mac", $c->{host}->{mac} ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_id", $id ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_origid", $orig_id ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_nr", $cnumber ); + readingsBulkUpdateIfChanged( $hash, "clients_${id}_group", $c->{config}->{group_id} ); + readingsEndUpdate( $hash, 1 ); + + my $clienthash = $defs{ $hash->{$id} } // return; + + readingsBeginUpdate($clienthash); + readingsBulkUpdateIfChanged( $clienthash, 'online', $c->{connected} ? 'true' : 'false' ); + readingsBulkUpdateIfChanged( $clienthash, 'name', $c->{config}->{name} ? $c->{config}->{name} : $c->{host}->{name} ); + readingsBulkUpdateIfChanged( $clienthash, 'latency', $c->{config}->{latency} ); + readingsBulkUpdateIfChanged( $clienthash, 'stream_id', $c->{config}->{stream_id} ); + readingsBulkUpdateIfChanged( $clienthash, 'volume', $c->{config}->{volume}->{percent} ); + readingsBulkUpdateIfChanged( $clienthash, 'muted', $c->{config}->{volume}->{muted} ? 'true' : 'false' ); + readingsBulkUpdateIfChanged( $clienthash, 'ip', $c->{host}->{ip} ); + readingsBulkUpdateIfChanged( $clienthash, 'mac', $c->{host}->{mac} ); + readingsBulkUpdateIfChanged( $clienthash, 'id', $id ); + readingsBulkUpdateIfChanged( $clienthash, 'origid', $orig_id ); + readingsBulkUpdateIfChanged( $clienthash, 'group', $c->{config}->{group_id} ); + readingsEndUpdate( $clienthash, 1 ); + + my $maxvol = Snapcast_getVolumeConstraint($clienthash); + Snapcast_setClient( $hash, $clienthash->{ID}, 'volume', $maxvol ) if $c->{config}->{volume}->{percent} > $maxvol; + return; +} + +sub Snapcast_updateStream { + my $hash = shift // return; + my $s = shift // return; + my $snumber = shift // return; + if ( $snumber == 0 ) { + $snumber++; + while ( defined $hash->{STATUS}->{streams}->{$snumber} && $s->{id} ne $hash->{STATUS}->{streams}->{$snumber}->{id} ) { + $snumber++; + } + return if !defined $hash->{STATUS}->{streams}->{$snumber}; + } + $hash->{STATUS}->{streams}->{$snumber} = $s; + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, "streams_${snumber}_id", $s->{id} ); + readingsBulkUpdateIfChanged( $hash, "streams_${snumber}_status", $s->{status} ); + readingsEndUpdate( $hash, 1 ); + return; +} + +sub Snapcast_Client_Register_Server { + my $hash = shift // return; + return if $hash->{MODE} ne 'client'; + my $name = $hash->{NAME} // return; + my $server = $hash->{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 + return; + } + $server = $defs{$server}; # get the server hash + $server->{ $hash->{ID} } = $name; + Snapcast_getStatus($server); + return; +} + +sub Snapcast_Client_Unregister_Server { + my $hash = shift // return; + return if $hash->{MODE} ne 'client'; + my $name = $hash->{NAME}; + my $server = $hash->{SERVER}; + $server = $defs{$server} // return; # get the server hash + readingsSingleUpdate( $server, "clients_$hash->{ID}_module", $name, 1 ); + delete $server->{ $hash->{ID} }; + return; +} + +sub Snapcast_getStatus { + my $hash = shift // return; + return Snapcast_Do( $hash, 'Server.GetStatus', '' ); +} + +sub Snapcast_parseStatus { + my $hash = shift // return; + my $status = shift // return; + my $streams = $status->{result}->{server}->{streams}; + my $groups = $status->{result}->{server}->{groups}; + my $server = $status->{result}->{server}->{server}; + + $hash->{STATUS}->{server} = $server; + + #readingsBeginUpdate($hash) if defined $groups || defined $streams; + if ( defined $groups ) { + my @groups = @{$groups}; + my $gnumber = 1; + my $cnumber = 1; + for my $g (@groups) { + my $groupstream = $g->{stream_id}; + my $groupid = $g->{id}; + my $clients = $g->{clients}; + if ( defined $clients ) { + my @clients = @{$clients}; + for my $c (@clients) { + $c->{config}->{stream_id} = $groupstream; # insert "stream" field for every client + $c->{config}->{group_id} = $groupid; # insert "group_id" field for every client + Snapcast_updateClient( $hash, $c, $cnumber ); + $cnumber++; + } + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, 'clients', $cnumber - 1 ); + readingsEndUpdate( $hash, 1 ); + } + } + } + if ( defined $streams ) { + my @streams = @{$streams}; + my $snumber = 1; + for my $s (@streams) { + Snapcast_updateStream( $hash, $s, $snumber ); + $snumber++; + } + readingsBeginUpdate($hash); + readingsBulkUpdateIfChanged( $hash, 'streams', $snumber - 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. + return; +} + +sub Snapcast_setClient { + my ( $hash, $id, $param, $value ) = @_; + my $name = $hash->{NAME}; + Log3( $name, 4, "SNAP setClient: $hash->{NAME}, id $id, param $param, val $value" ); + my $cnumber = ReadingsVal( $name, "clients_${id}_nr", undef ) // return; + my $method = $Snapcast_clientmethods{$param} // return; + + my $paramset->{id} = Snapcast_getId( $hash, $id ); + Log3( $name, 4, "SNAP setClient still there: $hash->{NAME}, paramsetid $paramset->{id}" ); + + if ( $param eq 'volumeConstraint' ) { + my @values = split m{ }x, $value; + return 'not enough parameters for volumeConstraint' if @values < 2; + + #my $match; + if ( @values % 2 ) { # there is a match argument given because number is uneven + #$match = pop @values; + pop @values; + } #else { + #$match = '_global_'; + #} + 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 volumeformat 0 - 100 for time/volume pair' if $values[ $i + 1 ] !~ /^(0?[0-9]?[0-9]|100)$/; + } + return; + } + + 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 + my $totalstreams = ReadingsVal( $name, 'streams', 0 ); + my $currentstream = Snapcast_getStreamNumber( $hash, ReadingsVal( $name, "clients_${id}_stream_id", '' ) ); + my $newstream = $currentstream + 1; + $newstream = 1 if $newstream > $totalstreams; #unless $newstream <= $totalstreams; + $value = ReadingsVal( $name, "streams_" . $newstream . "_id", '' ); + } + } + + my $muteState = ReadingsVal( $name, "clients_${id}_muted", 'false' ); my $volumeobject->{muted} = $muteState; - $volumeobject->{percent} = $value+0; - $value = $volumeobject; - } + my $currentVol = ReadingsVal( $name, "clients_${id}_volume", 50 ); - if($param eq "mute" ){ - my $currentVol = ReadingsVal($name,"clients_".$id."_volume",""); - my $volumeobject->{muted} = $value; - $volumeobject->{percent} = $currentVol+0; - $value = $volumeobject; - - if(not (defined($value->{muted})) || $value->{muted} eq ''){ - my $muteState = ReadingsVal($name,"clients_".$id."_muted",""); - my $currentVol = ReadingsVal($name,"clients_".$id."_volume",""); - $value = $muteState eq "true" || $muteState == 1 ? "false" : "true"; - my $volumeobject->{muted} = $value; - $volumeobject->{percent} = $currentVol+0; - $value = $volumeobject; + if ( $param eq 'volume' ) { + + #return undef unless defined($currentVol); + return if !$value; + + # check if volume was given as increment or decrement, then find out current volume and calculate new volume + if ( $value =~ /^([\+\-])(\d{1,2})$/ ) { + my $direction = $1; + my $amount = $2; + + #$value = eval($currentVol. $direction. $amount); + $value = eval { $currentVol . $direction . $amount }; + $value = 100 if $value > 100; + $value = 0 if $value < 0; + } + + # if volume is given with up or down argument, then increase or decrease according to volumeStepSize + if ( $value =~ m{\A(?:up|down)\z}x ) { + my $step = AttrVal( $name, 'volumeStepSizeThreshold', 5 ) > $currentVol ? AttrVal( $name, 'volumeStepSizeSmall', 1 ) : AttrVal( $name, 'volumeStepSize', 5 ); + if ( $value eq 'up' ) { + $value = $currentVol + $step; + } + else { + $value = $currentVol - $step; + } + $value = 100 if $value > 100; + $value = 0 if $value < 0; + $muteState = 'false' if $value > 0 && ( $muteState eq 'true' || $muteState == 1 ); + } + $volumeobject->{percent} = $value + 0; + $value = $volumeobject; } - $param = "volume"; # change param to "volume" to match new format - } - if(looks_like_number($value)){ - $paramset->{"$param"} = $value+0; - }else{ - $paramset->{"$param"} = $value - } - Snapcast_Do($hash,$method,$paramset); - return undef; -} + if ( $param eq 'mute' ) { + $volumeobject->{percent} = $currentVol + 0; + $value = $volumeobject; -sub Snapcast_Do($$$){ - my ($hash,$method,$param) = @_; - my $name = $hash->{NAME}; - $param = '' unless defined($param); - return DevIo_SimpleWrite($hash,Snapcast_Encode($hash,$method,$param),2); -} - -sub Snapcast_Encode($$$){ - my ($hash,$method,$param) = @_; - my $name = $hash->{NAME}; - 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 - my $request; - my $json; - $request->{jsonrpc}="2.0"; - $request->{method}=$method; - $request->{id}=$hash->{helper}{REQID}; - $request->{params} = $param unless $param eq ''; - $hash->{"IDLIST"}->{$request->{id}} = $request; - $request->{id}=$request->{id}+0; - $json=encode_json($request)."\r\n"; - $json =~s/\"true\"/true/; # Snapcast needs bool values without "" but encode_json does not do this - $json =~s/\"false\"/false/; - return $json; -} - -sub Snapcast_getStreamNumber($$){ - my ($hash,$id) = @_; - my $name = $hash->{NAME}; - for(my $i=1;$i<=ReadingsVal($name,"streams",1);$i++){ - if ($id eq ReadingsVal($name,"streams_".$i."_id","")){ - return $i; + if ( !defined $value->{muted} || $value->{muted} eq '' ) { + $value = $muteState eq 'true' || $muteState == 1 ? 'false' : 'true'; + $volumeobject->{muted} = $value; + $volumeobject->{percent} = $currentVol + 0; + $value = $volumeobject; + } + $param = 'volume'; # change param to "volume" to match new format } - } - return undef; -} -sub Snapcast_getId($$){ - my ($hash,$client) = @_; - my $name = $hash->{NAME}; - if($client=~/^([0-9a-f]{12}(\#*\d*|$))$/i){ # client is ID - for(my $i=1;$i<=ReadingsVal($name,"clients",1);$i++){ - if ($client eq $hash->{STATUS}->{clients}->{"$i"}->{id}) { - return $hash->{STATUS}->{clients}->{"$i"}->{origid}; - } + if ( ref $value ne 'HASH' && looks_like_number($value) ) { + $paramset->{$param} = $value + 0; } - } - return "unknown client"; -} - -sub Snapcast_getVolumeConstraint{ - my ($hash,$client) = @_; - my $name = $hash->{NAME}; - my $value = 100; - return $value if($hash->{MODE} ne "client"); - my @constraints=split(",",AttrVal($name,"constraints","")); - return $value if @constraints<1; - my $phase = ReadingsVal(AttrVal($name,"constraintDummy","undefined"),"state","standard"); - - foreach my $c (@constraints){ - my ($cname,$list)= split(/\|/,$c); - Log3 $name,3,"SNAP cname: $cname, list: $list"; - if($cname eq $phase){ - my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time+86400); - my $tomorrow=sprintf("%04d",1900+$year)."-".sprintf("%02d",$mon+1)."-".sprintf("%02d",$mday)." "; - $list =~ s/^\s+//; # get rid of whitespaces - $list =~ s/\s+$//; - my @listelements=split(" ", $list); - my $mindiff=time_str2num($tomorrow."23:59:00"); # eine Tageslänge - for(my $i=0;$i<@listelements/2;$i++){ - my $diff=abstime2rel($listelements[$i*2].":00"); # wie lange sind wir weg von der SChaltzeit? - if(time_str2num($tomorrow.$diff)<$mindiff){$mindiff=time_str2num($tomorrow.$diff);$value=$listelements[1+($i*2)];} # wir suchen die kleinste relative Zeit - } + else { + $paramset->{$param} = $value; } - } - readingsSingleUpdate($hash,"maxvol",$value,1); - return $value; # der aktuelle Auto-Wert wird zurückgegeben + + Snapcast_Do( $hash, $method, $paramset ); + return; } -sub Snapcast_isPmInstalled($$) -{ - my ($hash,$pm) = @_; - my ($name,$type) = ($hash->{NAME},$hash->{TYPE}); - if (not eval "use $pm;1") - { - Log3 $name, 1, "$type $name: perl modul missing: $pm. Install it, please."; - return "failed: $pm"; - } - - return undef; +sub Snapcast_Do { + my $hash = shift // return; + my $method = shift // return; + my $param = shift // ''; + my $payload = Snapcast_Encode( $hash, $method, $param ); + + #Log3($hash,5,"SNAP: Do $payload"); + return DevIo_SimpleWrite( $hash, $payload, 2 ); } + +sub Snapcast_Encode { + my $hash = shift // return; + my $method = shift // return; + my $param = 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 + my $request; + my $json; + $request->{jsonrpc} = "2.0"; + $request->{method} = $method; + $request->{id} = $hash->{helper}{REQID}; + $request->{params} = $param if $param ne ''; + $hash->{IDLIST}->{ $request->{id} } = $request; + $request->{id} = $request->{id} + 0; + $json = encode_json($request) . "\r\n"; + $json =~ s/\"true\"/true/; # Snapcast needs bool values without "" but encode_json does not do this + $json =~ s/\"false\"/false/; + return $json; +} + +sub Snapcast_getStreamNumber { + my $hash = shift // return; + my $id = shift // return; + my $name = $hash->{NAME} // return; + for my $i ( 1 .. ReadingsVal( $name, 'streams', 1 ) ) { + return $i if $id eq ReadingsVal( $name, "streams_${i}_id", '' ); + } + return; +} + +sub Snapcast_getId { + my $hash = shift // return; + my $client = shift // return; + + my $name = $hash->{NAME} // return; + if ( $client =~ m/^([0-9a-f]{12}(\#*\d*|$))$/i ) { # client is ID + for my $i ( 1 .. ReadingsVal( $name, 'streams', 1 ) ) { + return $hash->{STATUS}->{clients}->{$i}->{origid} + if $client eq $hash->{STATUS}->{clients}->{$i}->{id}; + } + } + return 'unknown client'; +} + +sub Snapcast_getVolumeConstraint { + my $hash = shift // return; + my $client = shift // return; + + my $name = $hash->{NAME} // return; + my $value = 100; + + return $value if $hash->{MODE} ne 'client'; + my @constraints = split m{,}x, AttrVal( $name, 'constraints', '' ); + return $value if !@constraints; + my $phase = ReadingsVal( AttrVal( $name, 'constraintDummy', undef ), 'state', undef ) // return $value; + + for my $c (@constraints) { + my ( $cname, $list ) = split m{\|}x, $c; + + #Log3($name,3,"SNAP cname: $cname, list: $list"); + if ( $cname eq $phase ) { + my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst ) = localtime( time + 86400 ); + my $tomorrow = sprintf( "%04d", 1900 + $year ) . "-" . sprintf( "%02d", $mon + 1 ) . "-" . sprintf( "%02d", $mday ) . " "; + $list = trim($list); #=~ s/^\s+//; # get rid of whitespaces + #$list =~ s/\s+$//; + my @listelements = split m{ }x, $list; + my $mindiff = time_str2num( $tomorrow . '23:59:00' ); # eine Tageslänge + for ( my $i = 0; $i < @listelements / 2; $i++ ) { + my $diff = abstime2rel( $listelements[ $i * 2 ] . ':00' ); # wie lange sind wir weg von der SChaltzeit? + if ( time_str2num( $tomorrow . $diff ) < $mindiff ) { $mindiff = time_str2num( $tomorrow . $diff ); $value = $listelements[ 1 + ( $i * 2 ) ]; } # wir suchen die kleinste relative Zeit + } + } + } + readingsSingleUpdate( $hash, 'maxvol', $value, 1 ); + return $value; # der aktuelle Auto-Wert wird zurückgegeben +} + + 1; __END__ diff --git a/fhem/FHEM/lib/AttrTemplate/mqtt2.template b/fhem/FHEM/lib/AttrTemplate/mqtt2.template index da6f55aca..0dd68d4c2 100644 --- a/fhem/FHEM/lib/AttrTemplate/mqtt2.template +++ b/fhem/FHEM/lib/AttrTemplate/mqtt2.template @@ -1,4 +1,5 @@ ########################################### +########################################### # $Id$ # # Comments start with #. Empty lines are ignored. @@ -4466,7 +4467,7 @@ deletereading -q OMG_BT_ID (?!associatedWith|IODev).* attr OMG_BT_ID devicetopic DEV_TPC attr OMG_BT_ID autocreate 0 attr OMG_BT_ID readingList\ - BASE_ID/(O[^/]*M[^/]*G[^/]*)/BTtoMQTT/BT_ID:.* { $EVENT =~ m,characteristic...0x2a19.*read[^\d]+([\d]+), ? return { batteryPercent => hex($1) } : $TOPIC =~ m,home/(O[^/]*M[^/]*G[^/]*)/BTtoMQTT,; my $rets = json2nameValue($EVENT); $rets->{last_IO} = $1, $rets->{"rssi_$1"} = $rets->{rssi}; return $rets} + BASE_ID/(O[^/]*M[^/]*G[^/]*)/BTtoMQTT/BT_ID:.* { $EVENT =~ m,characteristic...0x2a19.*read[^\d]+([\d]+), ? return { batteryPercent => hex($1) } : $TOPIC =~ m,BASE_ID/(O[^/]*M[^/]*G[^/]*)/BTtoMQTT,; my $rets = json2nameValue($EVENT); $rets->{last_IO} = $1, $rets->{"rssi_$1"} = $rets->{rssi}; return $rets} attr OMG_BT_ID getList batteryPercent:noArg batteryPercent { my $id = ReadingsVal($NAME,'id','BT_ID'); qq($\DEVICETOPIC/commands/MQTTtoBT/config {"ble_read_address":"$id","ble_read_service":"180f","ble_read_char":"2a19","value_type":"HEX"}) } attr OMG_BT_ID setList beep:noArg { my $id = ReadingsVal($NAME,'id','BT_ID'); qq($\DEVICETOPIC/commands/MQTTtoBT/config {"ble_read_address":"$id","ble_read_service":"180f","ble_read_char":"2a19","value_type":"HEX","immediate":true}) } attr OMG_BT_ID event-on-change-reading .* @@ -4477,7 +4478,7 @@ attr OMG_BT_ID room NEWDEVROOM { fhem "trigger $FW_wname JS:location.href='$FW_ME?detail=OMG_BT_ID'" if($cl && $cl->{TYPE} eq 'FHEMWEB') } attr OMG_BT_ID model OpenMQTTGateway_BT_gtag set DEVICE attrTemplate set_IODev_in_channels SUBCHANNELS=OMG_BT_ID -setreading OMG_BT_ID attrTemplateVersion 20220307 +setreading OMG_BT_ID attrTemplateVersion 20220705 name:OpenMQTTGateway_BT_mi_flora_sensor