From 3e544d6f835a8f09a7bc90448a5ebaf84e242b9b Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Tue, 19 Aug 2014 15:28:25 +0200 Subject: [PATCH 01/16] Modified the documentation to POD format and added the function to play a file/URL --- lib/xPL_Squeezebox.pm | 137 +++++++++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 42 deletions(-) diff --git a/lib/xPL_Squeezebox.pm b/lib/xPL_Squeezebox.pm index 765ce9273..090df2b4d 100644 --- a/lib/xPL_Squeezebox.pm +++ b/lib/xPL_Squeezebox.pm @@ -1,19 +1,15 @@ -=begin comment -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +=head1 B -xPL_Squeezebox.pm - xPL support for the former SlimDevices (now Logitech) Squeezebox devices +=head2 DESCRIPTION - $Date$ - $Revision$ +xPL_Squeezebox.pm - xPL support for the former SlimDevices (now Logitech) Squeezebox devices -Info: +This module allows to easily integrate Squeezebox devices in your MH setup by using the xPL +interface to the devices. - This module allows to easily integrate Squeezebox devices in your MH setup. +It supports device monitoring (check the heartbeat), keeps the state of the +squeezebox (playing/stopped/power_off), and allows to play a file/URL. - It supports device monitoring (check the heartbeat), keeps the state of the - squeezebox (playing/stopped/power_off), allows to play sounds while - maintaining the current playlist. - Usage: In your items.mht, add the squeezebox devices like this: @@ -39,31 +35,18 @@ Usage: set $sb_status_req_timer 60; xPL_Squeezebox::request_all_stat(); } - - Turn on debug=xpl_squeezebox for diagnostic messages - + +Turn on debug=xpl_squeezebox for diagnostic messages + Currently supports: * Turning the SB on/off (play/stop command) * Keeping track of the status of the SB when it is controlled by the remote/web interface + * Playing a file or an URL on a Squeezebox -Todo: - * Add code to control the amplifier based on the state of the SB - * Add code to pause the current playlist, play a certain file, and resume so that we can use the - SB to notify incoming calls/doorbell/... - * Support displaying messages on the SB screen - * Add internal status request timer - -License: - This free software is licensed under the terms of the GNU public license. +=head2 METHODS -Authors: - Lieven Hollevoet lieven@lika.be +=over -Credits: - Gregg Liming for the idea that we should not rely on the heartbeat messages to get the - status of the Squeezebox. - -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ =cut use strict; @@ -73,6 +56,12 @@ use base qw(xPL_Item); our @device_list; +=item C + +Creates a new object + +=cut + sub new { my ($class, $p_source) = @_; my $source = 'slimdev-slimserv.' . $p_source; @@ -91,36 +80,64 @@ sub new { return $self; } - -# Craft a message to request the state of all squeezeboxen -# We need to do this through this rather ugly code that keeps a list of all objects that have been created -# and that goes over this list one by one. -# This is because SqueezeCenter currently does not respond to an audio.request that is directed to -# slimdev-slimserv.* -# If it would we could here simply use -# &xPL::sendXpl('slimdev-slimserv.*', 'cmnd', 'audio.request' => { 'cmd' => 'status' }); +=item C + +Requests the state of all squeezebox devices + +We need to do this through this rather elaborate code that keeps a list of all objects +that have been created and that goes over this list one by one. +This is because SqueezeCenter currently does not respond to an audio.request that is directed to +slimdev-slimserv.* +If it would we could here simply use +&xPL::sendXpl('slimdev-slimserv.*', 'cmnd', 'audio.request' => { 'cmd' => 'status' }); + +=cut + sub request_all_stat { foreach (@device_list) { - $_->SUPER::send_cmnd('audio.request' => { 'cmd' => 'status' }); + $_->request_stat(); } } -# Request the status of the device +=item C + +Request the status of a single Squeezebox + +=cut + sub request_stat { my ($self) = @_; $self->SUPER::send_cmnd('audio.request' => { 'cmd' => 'status' }); } +=item C + +Returns the ID of the Squeezebox + +=cut + sub id { my ($self) = @_; return $$self{id}; } +=item C + +Add states to the device + +=cut + sub addStates { my $self = shift; push(@{$$self{states}}, @_) unless $self->{displayonly}; } +=item C + +Determine what xPL messages will be interpreted + +=cut + sub ignore_message { my ($self, $p_data) = @_; my $ignore_msg = 0; @@ -130,6 +147,12 @@ sub ignore_message { return $ignore_msg; } +=item C + +Handle state changes of the Squeezeboxes + +=cut + sub default_setstate { my ($self, $state, $substate, $set_by) = @_; @@ -139,7 +162,7 @@ sub default_setstate . " state is $state") if $main::Debug{xpl_squeezebox}; # TO-DO: process all of the other pertinent attributes available return -1 if $self->state eq $state; # don't propagate state unless it has changed - } + } } else { my $cmnd = ($state =~ /^off/i) ? 'stop' : 'play'; @@ -158,5 +181,35 @@ sub default_setstate } } - + +=item C + +Accepts a file (a full paths on the machine that is running the Squeezebox server +application) or an URL that should be played on the device. + +=cut + +sub play +{ + my ($self, $file, $source) = @_; + + $self->SUPER::send_cmnd('audio.slimserv' => {'command' => $file} ); + + return; +} + +=back + +=head2 LICENSE + +This free software is licensed under the terms of the GNU public license. + +=head2 AUTHOR + +Lieven Hollevoet lieven@lika.be + +=head2 CREDITS + Gregg Liming for the idea that we should not rely on the heartbeat messages to get the + status of the Squeezebox. + 1; From 39f281be41fb16f32253630372bdc1aab1e7fe35 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Tue, 19 Aug 2014 15:33:18 +0200 Subject: [PATCH 02/16] Added missing =cut statement at the end of the file --- lib/xPL_Squeezebox.pm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/xPL_Squeezebox.pm b/lib/xPL_Squeezebox.pm index 090df2b4d..9d1d1e9ac 100644 --- a/lib/xPL_Squeezebox.pm +++ b/lib/xPL_Squeezebox.pm @@ -212,4 +212,6 @@ Lieven Hollevoet lieven@lika.be Gregg Liming for the idea that we should not rely on the heartbeat messages to get the status of the Squeezebox. +=cut + 1; From dcef30d5170ca39873fec47cc804f8b79a8c38b3 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Mon, 25 Aug 2014 22:07:02 +0200 Subject: [PATCH 03/16] Alpha release supports parsing Squeezebox state --- lib/SqueezeboxCLI.pm | 208 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 lib/SqueezeboxCLI.pm diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm new file mode 100644 index 000000000..131470d87 --- /dev/null +++ b/lib/SqueezeboxCLI.pm @@ -0,0 +1,208 @@ +=head1 B + +=head2 SYNOPSIS + +The module enables control of a Squeezebox device through the CLI (command line +interface) of the Squeezebox server (a.k.a. Logitech Media server). + +=head2 CONFIGURATION + +This module connects to the Squeezebox server through the telnet interface. The following +preparations need to be done to get the code up and running: + +Create the Squeezebox devices in the mht file or in user code: + +.mht file: + + CODE, require SqueezeboxCLI; #noloop + CODE, $sb_living = new SqueezeboxCLI('living', ); #noloop + CODE, $sb_kitchen = new SqueezeboxCLI('kitchen', ); #noloop + +=head2 OVERVIEW + +This module allows control over a player through the telnet command line interface of +the server. + +=cut +package SqueezeboxCLI; + +# Used solely to provide a consistent logging feature, copied from Nest.pm + +use strict; + +#log levels +my $warn = 1; +my $info = 2; +my $trace = 3; + +sub debug { + my ($self, $message, $level) = @_; + $level = 0 if $level eq ''; + my $line = ''; + my @caller = caller(0); + if ($::Debug{'squeezeboxcli'} >= $level || $level == 0){ + $line = " at line " . $caller[2] if $::Debug{'squeezeboxcli'} >= $trace; + ::print_log("[" . $caller[0] . "] " . $message . $line); + } +} + +package SqueezeboxCLI_Interface; + +use strict; + +@SqueezeboxCLI_Interface::ISA = ('Generic_Item', 'SqueezeboxCLI'); + +sub new { + my ($class, $server, $port, $user, $pass) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{server} = $server; + $$self{port} = $port || 9090; + $$self{login} = $user . " " . $pass || ""; + $$self{players} = {}; + $$self{reconnect_timer} = new Timer; + $$self{squeezecenter} = new Socket_Item(undef, undef, $$self{server}. ":" . $$self{port}, "squeezecenter_cli", 'tcp', 'record'); + $self->login(); + ::MainLoop_pre_add_hook(sub {$self->check_for_data();}, 'persistent'); + return $self; +} + + +sub login { + my ($self) = @_; + + $self->debug( "Connecting to squeezecenter ..." ); + $self->{squeezecenter}->start(); + $self->{squeezecenter}->set_echo(0); + + if ($self->{login} ne " ") { + $self->{squeezecenter}->set('login ' . $self->{login}); + } + + $self->{squeezecenter}->set('listen 1'); +} + +sub reconnect { + my ($self) = @_; + + $self->{squeezecenter}->stop(); + $self->login(); + +} + +sub reconnect_delay { + my ($self, $seconds) = @_; + my $action = sub {$self->reconnect()}; + if (!$seconds) { + $seconds = 60; + $self->debug("Connection to squeezecenter lost, will try to connect again in 1 minute."); + } + $$self{reconnect_timer}->set($seconds,$action); +} + +sub check_for_data { + my ($self) = @_; + + unless ($$self{squeezecenter}->connected()) { + $self->reconnect_delay(); + return; + } + + if (my $data = $self->{squeezecenter}->said()) { + + # If we get a status response, check if we need to add the player to the lookup hash. + # This code will be executed after the status is requested in the 'add_player' routine. + # This is the only time we touch the actual server response, all other protocol specific + # code is implemented in SqueezeboxCLI_Player. + if ($data =~ /([\w|%]{27})\s+status\s+player_name%3A(\w+)/) { + my $player_mac = $1; + my $player_name = $2; + if (!defined($$self{players_mac}{$player_mac})) { + $self->debug("Adding $player_name to the MAC lookup", 2); + $$self{players_mac}{$player_mac} = $$self{players}{$player_name}; + return; + } + + } + + if ($data =~ /([\w|%]{27})\s+(.+)/) { + $self->debug("Passing message to player '$1' for further processing", 4); + # Pass the message to the correct object for processing + $$self{players_mac}{$1}->process_cli_response($2); + } else { + $self->debug("Received unknown text: $data"); + } + + # if ($data =~ m/.* power 0 .*$/) { +# main::print_log " power aus"; +# } elsif ($data =~ m/.* power 1 .*$/) { +# main::print_log " power an"; +# #set $EG_WZ_Multimedia ON; +# } elsif ($data =~ /([\w|%]+)\s+status\s+player_name%3A(\w+)/) { +# $self->debug("Got status response for $1 (= $2), adding it to the lookup hash", 1); +# +# $$self{players_mac}{$1} = shift(@{$self->{players}}); +# } else { +# main::print_log " unknown text: $data"; +# } + } +} + +sub add_player { + my ($self, $player) = @_; + + # Add the player to the list of players the gateway knows + $$self{players}{$player->{sb_name}} = $player; + $self->debug( "Added player '" . $player->{sb_name} . "'"); + + # Determine the MAC address of the player by requesting the status + $$self{squeezecenter}->set($player->{sb_name} . " status"); +} + + +package SqueezeboxCLI_Player; + +use strict; + +=head2 DEPENDENCIES + + URI::Escape - The CLI interface uses an escaped format + +=cut + +use URI::Escape; + +@SqueezeboxCLI_Player::ISA = ('Generic_Item', "SqueezeboxCLI"); + +sub new { + my ($class, $name, $interface) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{sb_name} = $name; + $$self{interface} = $interface; + $$self{interface}->add_player($self); + return $self; +} + +sub process_cli_response { + my ($self, $response) = @_; + $self->debug($self->get_object_name() . ": processing $response", 2); + + if ($response =~ /^power (\d)/) { + $self->debug($$self{object_name} . " power is " . $1); + } +} + + +=item C + +Handles setting the state of the object inside MisterHouse + +=cut + +sub set_receive { + my ($self, $p_state, $p_setby, $p_response) = @_; + $self->SUPER::set($p_state, $p_setby, $p_response); +} + +1; From 9378c5775a61216d9bb0bb9cba796f6d8060127e Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Mon, 8 Sep 2014 21:56:35 +0200 Subject: [PATCH 04/16] Tidied the file up to fix ugly formatting --- lib/SqueezeboxCLI.pm | 208 ++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 100 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 131470d87..fb36f845d 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -1,3 +1,4 @@ + =head1 B =head2 SYNOPSIS @@ -24,6 +25,7 @@ This module allows control over a player through the telnet command line interfa the server. =cut + package SqueezeboxCLI; # Used solely to provide a consistent logging feature, copied from Nest.pm @@ -36,13 +38,14 @@ my $info = 2; my $trace = 3; sub debug { - my ($self, $message, $level) = @_; + my ( $self, $message, $level ) = @_; $level = 0 if $level eq ''; - my $line = ''; + my $line = ''; my @caller = caller(0); - if ($::Debug{'squeezeboxcli'} >= $level || $level == 0){ - $line = " at line " . $caller[2] if $::Debug{'squeezeboxcli'} >= $trace; - ::print_log("[" . $caller[0] . "] " . $message . $line); + if ( $::Debug{'squeezeboxcli'} >= $level || $level == 0 ) { + $line = " at line " . $caller[2] + if $::Debug{'squeezeboxcli'} >= $trace; + ::print_log( "[" . $caller[0] . "] " . $message . $line ); } } @@ -50,116 +53,122 @@ package SqueezeboxCLI_Interface; use strict; -@SqueezeboxCLI_Interface::ISA = ('Generic_Item', 'SqueezeboxCLI'); +@SqueezeboxCLI_Interface::ISA = ( 'Generic_Item', 'SqueezeboxCLI' ); sub new { - my ($class, $server, $port, $user, $pass) = @_; - my $self = new Generic_Item(); - bless $self, $class; - $$self{server} = $server; - $$self{port} = $port || 9090; - $$self{login} = $user . " " . $pass || ""; - $$self{players} = {}; - $$self{reconnect_timer} = new Timer; - $$self{squeezecenter} = new Socket_Item(undef, undef, $$self{server}. ":" . $$self{port}, "squeezecenter_cli", 'tcp', 'record'); - $self->login(); - ::MainLoop_pre_add_hook(sub {$self->check_for_data();}, 'persistent'); - return $self; + my ( $class, $server, $port, $user, $pass ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{server} = $server; + $$self{port} = $port || 9090; + $$self{login} = $user . " " . $pass || ""; + $$self{players} = {}; + $$self{reconnect_timer} = new Timer; + $$self{squeezecenter} + = new Socket_Item( undef, undef, $$self{server} . ":" . $$self{port}, + "squeezecenter_cli", 'tcp', 'record' ); + $self->login(); + ::MainLoop_pre_add_hook( sub { $self->check_for_data(); }, 'persistent' ); + return $self; } - sub login { - my ($self) = @_; - - $self->debug( "Connecting to squeezecenter ..." ); + my ($self) = @_; + + $self->debug("Connecting to squeezecenter ..."); $self->{squeezecenter}->start(); $self->{squeezecenter}->set_echo(0); - - if ($self->{login} ne " ") { - $self->{squeezecenter}->set('login ' . $self->{login}); + + if ( $self->{login} ne " " ) { + $self->{squeezecenter}->set( 'login ' . $self->{login} ); } - - $self->{squeezecenter}->set('listen 1'); + + $self->{squeezecenter}->set('listen 1'); } -sub reconnect { - my ($self) = @_; - - $self->{squeezecenter}->stop(); +sub reconnect { + my ($self) = @_; + + $self->{squeezecenter}->stop(); $self->login(); } sub reconnect_delay { - my ($self, $seconds) = @_; - my $action = sub {$self->reconnect()}; - if (!$seconds) { + my ( $self, $seconds ) = @_; + my $action = sub { $self->reconnect() }; + if ( !$seconds ) { $seconds = 60; - $self->debug("Connection to squeezecenter lost, will try to connect again in 1 minute."); + $self->debug( + "Connection to squeezecenter lost, will try to connect again in 1 minute." + ); } - $$self{reconnect_timer}->set($seconds,$action); + $$self{reconnect_timer}->set( $seconds, $action ); } sub check_for_data { - my ($self) = @_; - - unless ($$self{squeezecenter}->connected()) { - $self->reconnect_delay(); - return; - } - - if (my $data = $self->{squeezecenter}->said()) { - - # If we get a status response, check if we need to add the player to the lookup hash. - # This code will be executed after the status is requested in the 'add_player' routine. - # This is the only time we touch the actual server response, all other protocol specific - # code is implemented in SqueezeboxCLI_Player. - if ($data =~ /([\w|%]{27})\s+status\s+player_name%3A(\w+)/) { - my $player_mac = $1; - my $player_name = $2; - if (!defined($$self{players_mac}{$player_mac})) { - $self->debug("Adding $player_name to the MAC lookup", 2); - $$self{players_mac}{$player_mac} = $$self{players}{$player_name}; - return; - } - - } - - if ($data =~ /([\w|%]{27})\s+(.+)/) { - $self->debug("Passing message to player '$1' for further processing", 4); - # Pass the message to the correct object for processing - $$self{players_mac}{$1}->process_cli_response($2); - } else { - $self->debug("Received unknown text: $data"); - } - - # if ($data =~ m/.* power 0 .*$/) { -# main::print_log " power aus"; + my ($self) = @_; + + unless ( $$self{squeezecenter}->connected() ) { + $self->reconnect_delay(); + return; + } + + if ( my $data = $self->{squeezecenter}->said() ) { + +# If we get a status response, check if we need to add the player to the lookup hash. +# This code will be executed after the status is requested in the 'add_player' routine. +# This is the only time we touch the actual server response, all other protocol specific +# code is implemented in SqueezeboxCLI_Player. + if ( $data =~ /([\w|%]{27})\s+status\s+player_name%3A(\w+)/ ) { + my $player_mac = $1; + my $player_name = $2; + if ( !defined( $$self{players_mac}{$player_mac} ) ) { + $self->debug( "Adding $player_name to the MAC lookup", 2 ); + $$self{players_mac}{$player_mac} + = $$self{players}{$player_name}; + return; + } + + } + + if ( $data =~ /([\w|%]{27})\s+(.+)/ ) { + $self->debug( + "Passing message to player '$1' for further processing", 4 ); + + # Pass the message to the correct object for processing + $$self{players_mac}{$1}->process_cli_response($2); + } + else { + $self->debug("Received unknown text: $data"); + } + +# if ($data =~ m/.* power 0 .*$/) { +# main::print_log " power aus"; # } elsif ($data =~ m/.* power 1 .*$/) { -# main::print_log " power an"; +# main::print_log " power an"; # #set $EG_WZ_Multimedia ON; # } elsif ($data =~ /([\w|%]+)\s+status\s+player_name%3A(\w+)/) { # $self->debug("Got status response for $1 (= $2), adding it to the lookup hash", 1); -# +# # $$self{players_mac}{$1} = shift(@{$self->{players}}); # } else { # main::print_log " unknown text: $data"; # } - } + } } sub add_player { - my ($self, $player) = @_; - - # Add the player to the list of players the gateway knows - $$self{players}{$player->{sb_name}} = $player; - $self->debug( "Added player '" . $player->{sb_name} . "'"); - - # Determine the MAC address of the player by requesting the status - $$self{squeezecenter}->set($player->{sb_name} . " status"); + my ( $self, $player ) = @_; + + # Add the player to the list of players the gateway knows + $$self{players}{ $player->{sb_name} } = $player; + $self->debug( "Added player '" . $player->{sb_name} . "'" ); + + # Determine the MAC address of the player by requesting the status + $$self{squeezecenter}->set( $player->{sb_name} . " status" ); } - - + package SqueezeboxCLI_Player; use strict; @@ -172,27 +181,26 @@ use strict; use URI::Escape; -@SqueezeboxCLI_Player::ISA = ('Generic_Item', "SqueezeboxCLI"); +@SqueezeboxCLI_Player::ISA = ( 'Generic_Item', "SqueezeboxCLI" ); sub new { - my ($class, $name, $interface) = @_; - my $self = new Generic_Item(); - bless $self, $class; - $$self{sb_name} = $name; - $$self{interface} = $interface; - $$self{interface}->add_player($self); - return $self; + my ( $class, $name, $interface ) = @_; + my $self = new Generic_Item(); + bless $self, $class; + $$self{sb_name} = $name; + $$self{interface} = $interface; + $$self{interface}->add_player($self); + return $self; } sub process_cli_response { - my ($self, $response) = @_; - $self->debug($self->get_object_name() . ": processing $response", 2); - - if ($response =~ /^power (\d)/) { - $self->debug($$self{object_name} . " power is " . $1); - } -} + my ( $self, $response ) = @_; + $self->debug( $self->get_object_name() . ": processing $response", 2 ); + if ( $response =~ /^power (\d)/ ) { + $self->debug( $$self{object_name} . " power is " . $1 ); + } +} =item C @@ -201,8 +209,8 @@ Handles setting the state of the object inside MisterHouse =cut sub set_receive { - my ($self, $p_state, $p_setby, $p_response) = @_; - $self->SUPER::set($p_state, $p_setby, $p_response); + my ( $self, $p_state, $p_setby, $p_response ) = @_; + $self->SUPER::set( $p_state, $p_setby, $p_response ); } 1; From 782803ae8fcd38dada9d5fc17479e35ea02de2ef Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Mon, 8 Sep 2014 22:43:20 +0200 Subject: [PATCH 05/16] We can turn a squeezebox on and off through the MisterHouse web interface. Achievement unlocked :-) --- lib/SqueezeboxCLI.pm | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index fb36f845d..b6dffb542 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -190,6 +190,8 @@ sub new { $$self{sb_name} = $name; $$self{interface} = $interface; $$self{interface}->add_player($self); + # Ensure we can turn the SB on and off + $self->addStates ('on', 'off'); return $self; } @@ -199,6 +201,7 @@ sub process_cli_response { if ( $response =~ /^power (\d)/ ) { $self->debug( $$self{object_name} . " power is " . $1 ); + } } @@ -208,9 +211,44 @@ Handles setting the state of the object inside MisterHouse =cut -sub set_receive { - my ( $self, $p_state, $p_setby, $p_response ) = @_; - $self->SUPER::set( $p_state, $p_setby, $p_response ); +#sub set_receive { +# my ( $self, $p_state, $p_setby, $p_response ) = @_; +# $self->SUPER::set( $p_state, $p_setby, $p_response ); +# $self->debug( $$self{object_name} . " setting from MH state to " . $p_state . " by " . $p_setby); +# print 'Test'; +#} + +=item C + +Handle state changes of the Squeezeboxes + +=cut + +sub default_setstate +{ + my ($self, $state, $substate, $set_by) = @_; + + my $cmnd = ($state =~ /^off/i) ? 'stop' : 'play'; + + return -1 if ($self->state eq $state); # Don't propagate state unless it has changed. + $self->debug("[SqueezeboxCLI] Request " . $self->get_object_name . " turn " . $cmnd ); + + if ($cmnd eq 'stop') { + $$self{interface}{squeezecenter}->set($$self{sb_name} . ' power 0'); + } else { + $$self{interface}{squeezecenter}->set($$self{sb_name} . ' power 1'); + } + } +=item C + +Add states to the device + +=cut + +sub addStates { + my $self = shift; + push(@{$$self{states}}, @_) unless $self->{displayonly}; +} 1; From 53bbf03cc73560711fe12d7529b0fa4a302545b2 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 14 Sep 2014 21:39:15 +0200 Subject: [PATCH 06/16] Added the option to control a 'coupled device' like an amplifier. Includes auto-off for the amplifier when the SB is paused --- lib/SqueezeboxCLI.pm | 157 +++++++++++++++++++++++++++++++++---------- 1 file changed, 120 insertions(+), 37 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index b6dffb542..d91e8279b 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -13,16 +13,32 @@ preparations need to be done to get the code up and running: Create the Squeezebox devices in the mht file or in user code: +Note: [parameters] are optional. + .mht file: CODE, require SqueezeboxCLI; #noloop - CODE, $sb_living = new SqueezeboxCLI('living', ); #noloop - CODE, $sb_kitchen = new SqueezeboxCLI('kitchen', ); #noloop + CODE, $squeezecenter = new SqueezeboxCLI_Interface('hostname'); #noloop + CODE, $sb_living = new SqueezeboxCLI('living', $squeezecenter, [coupled_device], [auto_off_time]); #noloop + CODE, $sb_kitchen = new SqueezeboxCLI('kitchen', $squeezecenter, [coupled_device], [auto_off_time]); #noloop + +Optional parameters: + +=over + +=item you can add a 'coupled device' to the Squeezebox. You would typically use this +when you want to switch the amplifier together with the Squeezebox. Couple a device with: + CODE, $sb_living->couple_device($amplifier_living); + +=item you can set an 'auto-off' time in minutes. When the player gets paused, you can define after how many minutes is should be turned off completely. +This is useful when you have defined a coupled device to avoid the amplifier to be on for too long after a playlist is paused. + +=back =head2 OVERVIEW -This module allows control over a player through the telnet command line interface of -the server. +This module allows to control and to monitor the state over a player through the telnet +command line interface of the server. =cut @@ -127,7 +143,6 @@ sub check_for_data { $self->debug( "Adding $player_name to the MAC lookup", 2 ); $$self{players_mac}{$player_mac} = $$self{players}{$player_name}; - return; } } @@ -167,6 +182,7 @@ sub add_player { # Determine the MAC address of the player by requesting the status $$self{squeezecenter}->set( $player->{sb_name} . " status" ); + } package SqueezeboxCLI_Player; @@ -184,39 +200,82 @@ use URI::Escape; @SqueezeboxCLI_Player::ISA = ( 'Generic_Item', "SqueezeboxCLI" ); sub new { - my ( $class, $name, $interface ) = @_; + my ( $class, $name, $interface, $coupled_device, $auto_off_time ) = @_; my $self = new Generic_Item(); bless $self, $class; - $$self{sb_name} = $name; - $$self{interface} = $interface; + $$self{sb_name} = $name; + $$self{interface} = $interface; + $$self{coupled_device} = $coupled_device || ""; + $$self{auto_off_time} = $auto_off_time || 0; + $$self{auto_off_timer} = new Timer; $$self{interface}->add_player($self); + # Ensure we can turn the SB on and off - $self->addStates ('on', 'off'); + $self->addStates( 'on', 'off' ); return $self; } sub process_cli_response { my ( $self, $response ) = @_; + + # Remove URI escape sequences + $response = uri_unescape($response); + + # Ignore the following messages, we're currently not using them + return if ( $response =~ /^prefset/ ); + return if ( $response =~ /^menustatus/ ); + $self->debug( $self->get_object_name() . ": processing $response", 2 ); - if ( $response =~ /^power (\d)/ ) { - $self->debug( $$self{object_name} . " power is " . $1 ); - + if ( $response =~ /power[:| ](\d)/ ) { + my $command = ( $1 == 1 ) ? 'ON' : 'OFF'; + $self->set( $command, 'cli' ); + $self->debug( $$self{object_name} + . " power is " + . $1 + . " command is $command" ); + + # Turn off the coupled device immediately if the SB is turned off + if ( $$self{coupled_device} ne "" && $command eq 'OFF' ) { + $$self{coupled_device}->set($command); + $$self{auto_off_timer}->unset(); + } + } -} + if ( $response =~ /mixer volume[:| ](\d+)/ ) { + $$self{mixer_volume} = $1; + $self->debug( $$self{object_name} . " mixer volume is " . $1 ); + } + if ( $response =~ /^pause (\d)/ || /mode[:| ]pause/ ) { -=item C + # If we are paused then maybe we need to fire the auto-off timer + if ( $1 == '1' ) { -Handles setting the state of the object inside MisterHouse + # Don't auto-off if the setting is '0'; + return if ( $$self{auto_off_time} == 0 ); -=cut + # Otherwise program the auto-off timer + my $action = sub { $self->default_setstate('off'); }; + $$self{auto_off_timer} + ->set( $$self{auto_off_time} * 60, $action ); + $self->debug( $$self{object_name} . " auto-off timer set" ); + } + } + if ( $response =~ /mode[:| ]play/ ) { -#sub set_receive { -# my ( $self, $p_state, $p_setby, $p_response ) = @_; -# $self->SUPER::set( $p_state, $p_setby, $p_response ); -# $self->debug( $$self{object_name} . " setting from MH state to " . $p_state . " by " . $p_setby); -# print 'Test'; -#} +# In case an auto-off timer is active we need to disable it when we start playing + $self->debug( + $$self{object_name} . " mode is playing, auto-timeoff cleared " ); + $$self{auto_off_timer}->unset(); + + # Control the coupled device too if it is defined + if ( $$self{coupled_device} ne "" ) { + $$self{coupled_device}->set('on'); + } + + } + +} =item C @@ -224,21 +283,31 @@ Handle state changes of the Squeezeboxes =cut -sub default_setstate -{ - my ($self, $state, $substate, $set_by) = @_; - - my $cmnd = ($state =~ /^off/i) ? 'stop' : 'play'; - - return -1 if ($self->state eq $state); # Don't propagate state unless it has changed. - $self->debug("[SqueezeboxCLI] Request " . $self->get_object_name . " turn " . $cmnd ); - - if ($cmnd eq 'stop') { - $$self{interface}{squeezecenter}->set($$self{sb_name} . ' power 0'); - } else { - $$self{interface}{squeezecenter}->set($$self{sb_name} . ' power 1'); +sub default_setstate { + my ( $self, $state, $substate, $set_by ) = @_; + + # If we're set by the CLI then we don't need to send out the command again + return -1 if ( $set_by eq 'cli' ); + + my $cmnd = ( $state =~ /^off/i ) ? 'stop' : 'play'; + + return -1 + if ( $self->state eq $state ) + ; # Don't propagate state unless it has changed. + $self->debug( "[SqueezeboxCLI] Request " + . $self->get_object_name + . " turn " + . $cmnd + . ' after ' + . $state ); + + if ( $cmnd eq 'stop' ) { + $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' power 0' ); } - + else { + $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' power 1' ); + } + } =item C @@ -249,6 +318,20 @@ Add states to the device sub addStates { my $self = shift; - push(@{$$self{states}}, @_) unless $self->{displayonly}; + push( @{ $$self{states} }, @_ ) unless $self->{displayonly}; +} + +=item C + +Couple another MisterHouse object to the Squeezebox device so that this device follows the +state of the Squeezebox. This can e.g. be used to switch an amplifier on when the +Squeezebox starts playing. + +=cut + +sub couple_device { + my ( $self, $device ) = @_; + + $$self{coupled_device} = $device; } 1; From 7cad70cdaa1bc99ef4c172203b62391806c2b5fb Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Wed, 17 Sep 2014 22:11:40 +0200 Subject: [PATCH 07/16] Code cleanup, first notification plays correctly and resumes playlist --- lib/SqueezeboxCLI.pm | 157 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 138 insertions(+), 19 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index d91e8279b..a5a268471 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -209,6 +209,8 @@ sub new { $$self{auto_off_time} = $auto_off_time || 0; $$self{auto_off_timer} = new Timer; $$self{interface}->add_player($self); + $$self{'notification_active'} = 0; + $$self{'notification_command_fired'} = 0; # Ensure we can turn the SB on and off $self->addStates( 'on', 'off' ); @@ -246,23 +248,24 @@ sub process_cli_response { $$self{mixer_volume} = $1; $self->debug( $$self{object_name} . " mixer volume is " . $1 ); } - if ( $response =~ /^pause (\d)/ || /mode[:| ]pause/ ) { - - # If we are paused then maybe we need to fire the auto-off timer - if ( $1 == '1' ) { - - # Don't auto-off if the setting is '0'; - return if ( $$self{auto_off_time} == 0 ); - - # Otherwise program the auto-off timer - my $action = sub { $self->default_setstate('off'); }; - $$self{auto_off_timer} - ->set( $$self{auto_off_time} * 60, $action ); - $self->debug( $$self{object_name} . " auto-off timer set" ); - } + if ( $response =~ /^pause 1/ || $response =~ /mode[:| ][pause|stop]/ ) { + $$self{mode} = 'pause'; + + # Don't auto-off if the setting is '0'; + return if ( $$self{auto_off_time} == 0 ); + + # Don't auto-off if we're already off + return if ( $self->state() eq 'OFF'); + + # Otherwise program the auto-off timer + my $action = sub { $self->default_setstate('off', 'timer'); }; + $$self{auto_off_timer} + ->set( $$self{auto_off_time} * 60, $action ); + $self->debug( $$self{object_name} . " auto-off timer set" ); + } if ( $response =~ /mode[:| ]play/ ) { - + $$self{mode} = 'play'; # In case an auto-off timer is active we need to disable it when we start playing $self->debug( $$self{object_name} . " mode is playing, auto-timeoff cleared " ); @@ -274,6 +277,29 @@ sub process_cli_response { } } + if ( $response =~ /playlist repeat[:| ](\d)/) { + $$self{repeat} = $1; + $self->debug( + $$self{object_name} . " repeat mode is $1" ); + } + if ( $response =~ /time (\d+.?\d+?)/) { + $$self{'time'} = $1; + $self->debug( + $$self{object_name} . " time is $1" ); + } + + # Restore the SB status when the notification is finisched playing + if ( $response =~ /playlist stop/ && $$self{notification_active}) { + $$self{notification_active} = 0; + $self->restore_sb_state(); + } + + # We need this to know when the notification is loaded, then the next 'done' means + # the notification is done. This way we don't need to poll and hence stall MisterHouse + if ( $response =~ /playlist load_done/ && $$self{notification_command_fired}) { + $$self{notification_active} = 1; + $$self{notification_command_fired} = 0; + } } @@ -294,18 +320,18 @@ sub default_setstate { return -1 if ( $self->state eq $state ) ; # Don't propagate state unless it has changed. - $self->debug( "[SqueezeboxCLI] Request " + $self->debug( "Request " . $self->get_object_name . " turn " . $cmnd . ' after ' . $state ); - if ( $cmnd eq 'stop' ) { - $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' power 0' ); + if ( $state =~ /^off/i ) { + $self->send_cmd( $$self{sb_name} . ' power 0' ); } else { - $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' power 1' ); + $self->send_cmd( $$self{sb_name} . ' power 1' ); } } @@ -334,4 +360,97 @@ sub couple_device { $$self{coupled_device} = $device; } + +=item C + +Play a notification on this squeezebox. The notification can either be a file or an URL. +This function stops the current playback, plays the notification and then returns the +Squeezebox to the previous state. Credits to @rudybrian for writing the first version +of this code and his permission to re-use it! + +=cut + +sub play_notification { + my ( $self, $notification ) = @_; + + # Save the state + $self->save_sb_state(); + + # Pause playback if required + if ($$self{mode} eq "play") { + $self->send_cmd("pause 1 1"); + } + + # Get the current playback position + $self->send_cmd("time ?"); + + # Save the current playlist + $self->send_cmd("playlist save prenotification_playlist"); + + # Set the repeat to none + $self->send_cmd("playlist repeat 0"); + + # Play notification + # Ensure we know we're playing a notification. + $$self{'notification_command_fired'} = 1; + + $self->send_cmd("playlist play $notification"); + #$self->send_cmd("mode play"); + +} + +=item C + +Saves the current state of the Squeezebox so that it can be restored later + +=cut + +sub save_sb_state { + my $self = shift; + + $$self{'prev_state'}->{'mode'} = $$self{'mode'}; + $$self{'prev_state'}->{'state'} = $self->state(); + $$self{'prev_state'}->{'repeat'} = $$self{'repeat'}; + $self->debug( + $$self{object_name} . " saved state to be: mode:" . $$self{'prev_state'}->{'mode'} . " state:" . $$self{'prev_state'}->{'state'} . " repeat:" . $$self{'prev_state'}->{'repeat'} ); + +} + +=item C + +Resume the Squeezebox state from the previously saved state + +=cut + +sub restore_sb_state { + my $self = shift; + + $self->debug( + $$self{object_name} . " restoring the SB state" ); + + # Restore playlist/mode + if ($$self{prev_state}->{'mode'} eq 'play') { + $self->send_cmd("playlist resume prenotification_playlist"); + $self->send_cmd("time " . $$self{'time'}); + } else { + $self->send_cmd("playlist resume prenotification_playlist noplay:1"); + } + + # And restore repeat state + $self->send_cmd("playlist repeat " . $$self{'prev_state'}->{'repeat'}); + +} + +=item C + +Helper function to send a command to the squeezebox over the CLI + +=cut + +sub send_cmd { + my ($self, $cmd) = @_; + $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' ' . $cmd ); + $self->debug( + $$self{object_name} . " sending command '$cmd'" ); +} 1; From 7530477f7b6349e29e0d0bf1e9b34faeb2e23c76 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 20 Sep 2014 23:43:21 +0200 Subject: [PATCH 08/16] First working version that allows auto-off and notification playback in all states of the player (off, paused, playing). Stores and restores the current playlist --- lib/SqueezeboxCLI.pm | 276 ++++++++++++++++++++++++------------------- 1 file changed, 152 insertions(+), 124 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index a5a268471..be1ab5ec0 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -35,10 +35,16 @@ This is useful when you have defined a coupled device to avoid the amplifier to =back +To play a file or URL from your user code you can use this function call: + +$sb_kitchen->play_notification('/Volumes/Media/speech/test1.wave'); + + =head2 OVERVIEW -This module allows to control and to monitor the state over a player through the telnet -command line interface of the server. +This module allows to control and to monitor the state over a Squeezebox player through the telnet +command line interface of the server. It also allows you to play notifications. Notifications +can either be local files or URLs. =cut @@ -158,18 +164,6 @@ sub check_for_data { $self->debug("Received unknown text: $data"); } -# if ($data =~ m/.* power 0 .*$/) { -# main::print_log " power aus"; -# } elsif ($data =~ m/.* power 1 .*$/) { -# main::print_log " power an"; -# #set $EG_WZ_Multimedia ON; -# } elsif ($data =~ /([\w|%]+)\s+status\s+player_name%3A(\w+)/) { -# $self->debug("Got status response for $1 (= $2), adding it to the lookup hash", 1); -# -# $$self{players_mac}{$1} = shift(@{$self->{players}}); -# } else { -# main::print_log " unknown text: $data"; -# } } } @@ -209,11 +203,11 @@ sub new { $$self{auto_off_time} = $auto_off_time || 0; $$self{auto_off_timer} = new Timer; $$self{interface}->add_player($self); - $$self{'notification_active'} = 0; + $$self{'notification_active'} = 0; $$self{'notification_command_fired'} = 0; # Ensure we can turn the SB on and off - $self->addStates( 'on', 'off' ); + $self->addStates( 'on', 'off', 'play', 'pause' ); return $self; } @@ -226,21 +220,27 @@ sub process_cli_response { # Ignore the following messages, we're currently not using them return if ( $response =~ /^prefset/ ); return if ( $response =~ /^menustatus/ ); + return if ( $response =~ /^displaynotify/ ); - $self->debug( $self->get_object_name() . ": processing $response", 2 ); + $self->debug( + $self->get_object_name() + . ": processing '$response', current state " + . $self->state(), + 2 + ); if ( $response =~ /power[:| ](\d)/ ) { - my $command = ( $1 == 1 ) ? 'ON' : 'OFF'; - $self->set( $command, 'cli' ); + my $command = ( $1 == 1 ) ? 'on' : 'off'; $self->debug( $$self{object_name} . " power is " . $1 . " command is $command" ); + $self->set_now( $command, 'cli' ); # Turn off the coupled device immediately if the SB is turned off - if ( $$self{coupled_device} ne "" && $command eq 'OFF' ) { + if ( $$self{coupled_device} ne "" && $command eq 'off' ) { $$self{coupled_device}->set($command); - $$self{auto_off_timer}->unset(); + $$self{auto_off_timer}->set(0); } } @@ -248,28 +248,45 @@ sub process_cli_response { $$self{mixer_volume} = $1; $self->debug( $$self{object_name} . " mixer volume is " . $1 ); } - if ( $response =~ /^pause 1/ || $response =~ /mode[:| ][pause|stop]/ ) { - $$self{mode} = 'pause'; - - # Don't auto-off if the setting is '0'; - return if ( $$self{auto_off_time} == 0 ); - - # Don't auto-off if we're already off - return if ( $self->state() eq 'OFF'); - - # Otherwise program the auto-off timer - my $action = sub { $self->default_setstate('off', 'timer'); }; - $$self{auto_off_timer} - ->set( $$self{auto_off_time} * 60, $action ); - $self->debug( $$self{object_name} . " auto-off timer set" ); - + if ( ( $response =~ /^pause 1/ || $response =~ /mode[:| ][pause|stop]/ ) + && $self->state() ne 'off' ) + { + $self->debug( $$self{object_name} + . " we got mode pause and state is " + . $self->state() ); + + $self->set_now( 'pause', 'cli' ); + + # Don't auto-off if the setting is '0'; + if ( $$self{auto_off_time} ) { + + # Program the auto-off timer + my $action = sub { $self->set( 'off', 'timer' ); }; + $$self{auto_off_timer} + ->set( $$self{auto_off_time} * 60, $action ); + $self->debug( $$self{object_name} + . " auto-off timer set because current state is " + . $self->state() ); + } + } + if ( ( $response =~ /^pause 0/ ) ) { + + # Request the current mode if pause is 0 + $self->send_cmd("mode ?"); } if ( $response =~ /mode[:| ]play/ ) { - $$self{mode} = 'play'; + $self->debug( $$self{object_name} + . " received mode play, now in " + . $self->state() ); + + $self->set_now( 'play', 'cli' ); + # In case an auto-off timer is active we need to disable it when we start playing - $self->debug( - $$self{object_name} . " mode is playing, auto-timeoff cleared " ); - $$self{auto_off_timer}->unset(); + $self->debug( $$self{object_name} + . " mode is " + . $self->state() + . ", auto-timeoff cleared " ); + $$self{auto_off_timer}->set(0); # Control the coupled device too if it is defined if ( $$self{coupled_device} ne "" ) { @@ -277,28 +294,28 @@ sub process_cli_response { } } - if ( $response =~ /playlist repeat[:| ](\d)/) { - $$self{repeat} = $1; - $self->debug( - $$self{object_name} . " repeat mode is $1" ); + if ( $response =~ /playlist repeat[:| ](\d)/ ) { + $$self{repeat} = $1; + $self->debug( $$self{object_name} . " repeat mode is $1" ); } - if ( $response =~ /time (\d+.?\d+?)/) { - $$self{'time'} = $1; - $self->debug( - $$self{object_name} . " time is $1" ); + if ( $response =~ /time (\d+.\d+)/ ) { + $$self{'time'} = $1; + $self->debug( $$self{object_name} . " time is $1" ); } - + # Restore the SB status when the notification is finisched playing - if ( $response =~ /playlist stop/ && $$self{notification_active}) { - $$self{notification_active} = 0; - $self->restore_sb_state(); + if ( $response =~ /playlist stop/ && $$self{notification_active} ) { + $$self{notification_active} = 0; + $self->restore_sb_state(); } - - # We need this to know when the notification is loaded, then the next 'done' means - # the notification is done. This way we don't need to poll and hence stall MisterHouse - if ( $response =~ /playlist load_done/ && $$self{notification_command_fired}) { - $$self{notification_active} = 1; - $$self{notification_command_fired} = 0; + +# We need this to know when the notification is loaded, then the next 'done' means +# the notification is done. This way we don't need to poll and hence stall MisterHouse + if ( $response =~ /playlist load_done/ + && $$self{notification_command_fired} ) + { + $$self{notification_active} = 1; + $$self{notification_command_fired} = 0; } } @@ -312,26 +329,32 @@ Handle state changes of the Squeezeboxes sub default_setstate { my ( $self, $state, $substate, $set_by ) = @_; + $self->debug( + $$self{object_name} . " in setstate with $state setby $set_by" ); + # If we're set by the CLI then we don't need to send out the command again - return -1 if ( $set_by eq 'cli' ); + # as we actually received it from the server + return if ( $set_by eq 'cli' ); - my $cmnd = ( $state =~ /^off/i ) ? 'stop' : 'play'; + # Don't propagate state unless it has changed. + return -1 if ( $self->state eq $state ); - return -1 - if ( $self->state eq $state ) - ; # Don't propagate state unless it has changed. + # Print debug info $self->debug( "Request " . $self->get_object_name - . " turn " - . $cmnd - . ' after ' - . $state ); + . " to change to state '$state' by '$set_by'" ); if ( $state =~ /^off/i ) { - $self->send_cmd( $$self{sb_name} . ' power 0' ); + $self->send_cmd('power 0'); } - else { - $self->send_cmd( $$self{sb_name} . ' power 1' ); + if ( $state =~ /^on/i ) { + $self->send_cmd('power 1'); + } + if ( $state =~ /^play/i ) { + $self->send_cmd('mode play'); + } + if ( $state =~ /^pause/i ) { + $self->send_cmd('mode pause'); } } @@ -371,32 +394,33 @@ of this code and his permission to re-use it! =cut sub play_notification { - my ( $self, $notification ) = @_; - - # Save the state - $self->save_sb_state(); - - # Pause playback if required - if ($$self{mode} eq "play") { - $self->send_cmd("pause 1 1"); - } - - # Get the current playback position - $self->send_cmd("time ?"); - - # Save the current playlist - $self->send_cmd("playlist save prenotification_playlist"); - - # Set the repeat to none - $self->send_cmd("playlist repeat 0"); - - # Play notification - # Ensure we know we're playing a notification. - $$self{'notification_command_fired'} = 1; - - $self->send_cmd("playlist play $notification"); - #$self->send_cmd("mode play"); - + my ( $self, $notification ) = @_; + + # Save the state + $self->save_sb_state(); + + # Pause playback if required + if ( $self->state() eq "play" ) { + $self->send_cmd("pause 1 1"); + } + + # Get the current playback position + $self->send_cmd("time ?"); + + # Save the current playlist + $self->send_cmd("playlist save prenotification_playlist"); + + # Set the repeat to none + $self->send_cmd("playlist repeat 0"); + + # Play notification + # Ensure we know we're playing a notification. + $$self{'notification_command_fired'} = 1; + + $self->send_cmd("playlist play $notification"); + + #$self->send_cmd("mode play"); + } =item C @@ -406,14 +430,16 @@ Saves the current state of the Squeezebox so that it can be restored later =cut sub save_sb_state { - my $self = shift; - - $$self{'prev_state'}->{'mode'} = $$self{'mode'}; - $$self{'prev_state'}->{'state'} = $self->state(); - $$self{'prev_state'}->{'repeat'} = $$self{'repeat'}; - $self->debug( - $$self{object_name} . " saved state to be: mode:" . $$self{'prev_state'}->{'mode'} . " state:" . $$self{'prev_state'}->{'state'} . " repeat:" . $$self{'prev_state'}->{'repeat'} ); - + my $self = shift; + + $$self{'prev_state'}->{'state'} = $self->state(); + $$self{'prev_state'}->{'repeat'} = $$self{'repeat'}; + $self->debug( $$self{object_name} + . " saved state to be: state:" + . $$self{'prev_state'}->{'state'} + . " repeat:" + . $$self{'prev_state'}->{'repeat'} ); + } =item C @@ -423,22 +449,25 @@ Resume the Squeezebox state from the previously saved state =cut sub restore_sb_state { - my $self = shift; - - $self->debug( - $$self{object_name} . " restoring the SB state" ); - - # Restore playlist/mode - if ($$self{prev_state}->{'mode'} eq 'play') { - $self->send_cmd("playlist resume prenotification_playlist"); - $self->send_cmd("time " . $$self{'time'}); - } else { - $self->send_cmd("playlist resume prenotification_playlist noplay:1"); - } - - # And restore repeat state - $self->send_cmd("playlist repeat " . $$self{'prev_state'}->{'repeat'}); - + my $self = shift; + + $self->debug( $$self{object_name} . " restoring the SB state" ); + + # Restore playlist/mode + if ( $$self{prev_state}->{'state'} eq 'play' ) { + $self->send_cmd("playlist resume prenotification_playlist"); + $self->send_cmd( "time " . $$self{'time'} ); + } + else { + $self->send_cmd("playlist resume prenotification_playlist noplay:1"); + } + + # And restore repeat state + $self->send_cmd( "playlist repeat " . $$self{'prev_state'}->{'repeat'} ); + +# Ensure we know the state of the device in this module, request the state explicitly + $self->send_cmd("mode ?"); + } =item C @@ -448,9 +477,8 @@ Helper function to send a command to the squeezebox over the CLI =cut sub send_cmd { - my ($self, $cmd) = @_; - $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' ' . $cmd ); - $self->debug( - $$self{object_name} . " sending command '$cmd'" ); + my ( $self, $cmd ) = @_; + $$self{interface}{squeezecenter}->set( $$self{sb_name} . ' ' . $cmd ); + $self->debug( $$self{object_name} . " sending command '$cmd'" ); } 1; From 8cbe339edb1fc23795322249d83dc0e81f405202 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 27 Sep 2014 22:01:01 +0200 Subject: [PATCH 09/16] Save the notification with a device-specific name to avoid confusion --- lib/SqueezeboxCLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index be1ab5ec0..7634d387c 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -408,7 +408,7 @@ sub play_notification { $self->send_cmd("time ?"); # Save the current playlist - $self->send_cmd("playlist save prenotification_playlist"); + $self->send_cmd("playlist save prenotification_playlist_" . $self{object_name}); # Set the repeat to none $self->send_cmd("playlist repeat 0"); From 513079343e859c1565805c45c4b577065ea2ce39 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 27 Sep 2014 22:23:13 +0200 Subject: [PATCH 10/16] Thou shall never commit code without testing, even if it is only a minor change --- lib/SqueezeboxCLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 7634d387c..f1e0f4701 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -408,7 +408,7 @@ sub play_notification { $self->send_cmd("time ?"); # Save the current playlist - $self->send_cmd("playlist save prenotification_playlist_" . $self{object_name}); + $self->send_cmd("playlist save prenotification_playlist_" . $$self{object_name}); # Set the repeat to none $self->send_cmd("playlist repeat 0"); From ef99cd3cbb59783d72fad9c36a907d538c141fcd Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 27 Sep 2014 22:37:02 +0200 Subject: [PATCH 11/16] Only try to parse messages for players we're subscribing for. When porting the code to my production server I noticed it crashes when information for a non-subscribed player was received. Added the proper checks. --- lib/SqueezeboxCLI.pm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index f1e0f4701..887c24bf6 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -19,8 +19,8 @@ Note: [parameters] are optional. CODE, require SqueezeboxCLI; #noloop CODE, $squeezecenter = new SqueezeboxCLI_Interface('hostname'); #noloop - CODE, $sb_living = new SqueezeboxCLI('living', $squeezecenter, [coupled_device], [auto_off_time]); #noloop - CODE, $sb_kitchen = new SqueezeboxCLI('kitchen', $squeezecenter, [coupled_device], [auto_off_time]); #noloop + CODE, $sb_living = new SqueezeboxCLI_Player('living', $squeezecenter, [coupled_device], [auto_off_time]); #noloop + CODE, $sb_kitchen = new SqueezeboxCLI_Player('kitchen', $squeezecenter, [coupled_device], [auto_off_time]); #noloop Optional parameters: @@ -158,7 +158,11 @@ sub check_for_data { "Passing message to player '$1' for further processing", 4 ); # Pass the message to the correct object for processing - $$self{players_mac}{$1}->process_cli_response($2); + # but only do this for players we're supposed to manage + my $player = $1; + if (defined $$self{players_mac}{$player}) { + $$self{players_mac}{$player}->process_cli_response($2); + } } else { $self->debug("Received unknown text: $data"); From 27ffc894cec969c34ef0c975eb35e39f07317bc4 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 27 Sep 2014 22:57:30 +0200 Subject: [PATCH 12/16] Cater for slightly different reporting of pause between Radio and Classic Squeezebox --- lib/SqueezeboxCLI.pm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 887c24bf6..4913f07a6 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -39,6 +39,7 @@ To play a file or URL from your user code you can use this function call: $sb_kitchen->play_notification('/Volumes/Media/speech/test1.wave'); +For additional debugging, add the option squeezeboxcli:3 to the 'debug' entry to your mh.ini.private file. =head2 OVERVIEW @@ -273,8 +274,8 @@ sub process_cli_response { . $self->state() ); } } - if ( ( $response =~ /^pause 0/ ) ) { - + if ( ( $response =~ /pause 0/ ) ) { + # Removed the /^ requirement because the classic player reports 'playlist pause 0' instead of 'pause 0' like the radio does # Request the current mode if pause is 0 $self->send_cmd("mode ?"); } From 3d47e1a84e454cd7885ee39c28ef9ed6b2adf446 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 27 Sep 2014 23:06:29 +0200 Subject: [PATCH 13/16] Ensure we capture the playing state in all situations --- lib/SqueezeboxCLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 4913f07a6..7d04e6bfc 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -274,7 +274,7 @@ sub process_cli_response { . $self->state() ); } } - if ( ( $response =~ /pause 0/ ) ) { + if ( $response =~ /pause 0/ || $response =~ /playlist newsong/ ) { # Removed the /^ requirement because the classic player reports 'playlist pause 0' instead of 'pause 0' like the radio does # Request the current mode if pause is 0 $self->send_cmd("mode ?"); From ad0fcbbfed8f928d7f172a09b99b3be7822c9e20 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Tue, 30 Sep 2014 21:44:15 +0200 Subject: [PATCH 14/16] Added the option to set the volume for the notification to play --- lib/SqueezeboxCLI.pm | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 7d04e6bfc..271c6a3c2 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -301,11 +301,15 @@ sub process_cli_response { } if ( $response =~ /playlist repeat[:| ](\d)/ ) { $$self{repeat} = $1; - $self->debug( $$self{object_name} . " repeat mode is $1" ); + $self->debug( $$self{object_name} . " repeat mode is $1", 3); } if ( $response =~ /time (\d+.\d+)/ ) { $$self{'time'} = $1; - $self->debug( $$self{object_name} . " time is $1" ); + $self->debug( $$self{object_name} . " time is $1", 3); + } + if ( $response =~ /volume[:| ](\d+)/){ + $$self{'volume'} = $1; + $self->debug( $$self{object_name} . " volume is $1", 3); } # Restore the SB status when the notification is finisched playing @@ -389,20 +393,27 @@ sub couple_device { $$self{coupled_device} = $device; } -=item C +=item C)> Play a notification on this squeezebox. The notification can either be a file or an URL. This function stops the current playback, plays the notification and then returns the Squeezebox to the previous state. Credits to @rudybrian for writing the first version of this code and his permission to re-use it! +You can pass an extra parameter that is used for the notification. + =cut sub play_notification { - my ( $self, $notification ) = @_; + my ( $self, $notification, $volume ) = @_; # Save the state $self->save_sb_state(); + + # Modify the volume if required + if (defined $volume && ($volume > 0 && $volume < 100 )) { + $self->send_cmd("mixer volume " . $volume); + } # Pause playback if required if ( $self->state() eq "play" ) { @@ -439,6 +450,8 @@ sub save_sb_state { $$self{'prev_state'}->{'state'} = $self->state(); $$self{'prev_state'}->{'repeat'} = $$self{'repeat'}; + $$self{'prev_state'}->{'volume'} = $$self{'volume'}; + $self->debug( $$self{object_name} . " saved state to be: state:" . $$self{'prev_state'}->{'state'} @@ -458,6 +471,9 @@ sub restore_sb_state { $self->debug( $$self{object_name} . " restoring the SB state" ); + # Restore the volume + $self->send_cmd("mixer volume " . $$self{'prev_state'}->{'volume'}); + # Restore playlist/mode if ( $$self{prev_state}->{'state'} eq 'play' ) { $self->send_cmd("playlist resume prenotification_playlist"); From cc4948e18e358cd0101710b6d89fa8a6be587d9c Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sat, 11 Oct 2014 17:50:52 +0200 Subject: [PATCH 15/16] Added amplifier pre-heat time and support to play playlists --- lib/SqueezeboxCLI.pm | 97 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index 271c6a3c2..ea8e9a605 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -198,8 +198,34 @@ use URI::Escape; @SqueezeboxCLI_Player::ISA = ( 'Generic_Item', "SqueezeboxCLI" ); +=item C + +Creates a Squeezebox_Player object. The following parameter are required: + +=over + +=item name: the 'friendly' name of the squeezebox in squeezecenter. This parameter is used to link this object to the correct status messages in the CLI interface of squeezecenter + +=item interface: the object that is the CLI interface to assign this player to. + +=back + +The following parameters are optional + +=over + +=item amplifier: the object that needs to be enabled and disabled together with the squeezebox + +=item auto_off_time: the time (in minutes) the squeezebox and the optional attached amplifier should be turned off after a playlist has ended + +=item preheat_time: the time (in seconds) the amplifier should be turned on before a notification is played if the amplifier is off. This enables the amplifier to turn on and enable the speakers before the notification is played. + +=back + +=cut + sub new { - my ( $class, $name, $interface, $coupled_device, $auto_off_time ) = @_; + my ( $class, $name, $interface, $coupled_device, $auto_off_time, $preheat_time ) = @_; my $self = new Generic_Item(); bless $self, $class; $$self{sb_name} = $name; @@ -207,6 +233,8 @@ sub new { $$self{coupled_device} = $coupled_device || ""; $$self{auto_off_time} = $auto_off_time || 0; $$self{auto_off_timer} = new Timer; + $$self{preheat_time} = $preheat_time || 0; + $$self{preheat_timer} = new Timer; $$self{interface}->add_player($self); $$self{'notification_active'} = 0; $$self{'notification_command_fired'} = 0; @@ -216,6 +244,13 @@ sub new { return $self; } +=item C + +Interprete the data that is received from the CLI interface. Called from the +gateway module. + +=cut + sub process_cli_response { my ( $self, $response ) = @_; @@ -402,11 +437,29 @@ of this code and his permission to re-use it! You can pass an extra parameter that is used for the notification. +Note: currently this function does not support multiple notifications being pushed at the same time + =cut sub play_notification { + my ( $self, $notification, $volume ) = @_; + # If the amplifier is defined and it is not on and we need to preheat, let's do it + if ($$self{'coupled_device'} ne '' and $$self{'coupled_device'}->state() ne 'on' and $$self{'preheat_time'}) { + + $self->debug( $$self{object_name} + . " Preheating the amplifier and delaying the notification. We'll be back when the speakers are on"); + + # Turn on + $$self{coupled_device}->set('on'); + + # Program timer to play the notification later + my $action = sub { $self->play_notification($notification, $volume); }; + $$self{preheat_timer}->set( $$self{preheat_time}, $action ); + return; + } + # Save the state $self->save_sb_state(); @@ -439,6 +492,46 @@ sub play_notification { } +=item C + +Changes the playlist to a new media file + +Parameters are the media (a file or an URL) and optionally the volume at which to play the media. + +=cut + +sub play { + my ($self, $playlist, $volume) = @_; + + # Modify the volume if required + if (defined $volume && ($volume > 0 && $volume < 100 )) { + $self->send_cmd("mixer volume " . $volume); + } + + $self->send_cmd("playlist play $playlist"); +} + +=item C + +Changes the playlist to a new playlist. + +Parameters are the playlist name and optionally the volume at which to play the media. + +=cut + +sub play_playlist { + my ($self, $playlist, $volume) = @_; + + # Modify the volume if required + if (defined $volume && ($volume > 0 && $volume < 100 )) { + $self->send_cmd("mixer volume " . $volume); + } + + $self->send_cmd("playlistcontrol cmd:load playlist_name:$playlist"); + +} + + =item C Saves the current state of the Squeezebox so that it can be restored later @@ -476,7 +569,7 @@ sub restore_sb_state { # Restore playlist/mode if ( $$self{prev_state}->{'state'} eq 'play' ) { - $self->send_cmd("playlist resume prenotification_playlist"); + $self->send_cmd("playlist resume prenotification_playlist_" . $$self{object_name}); $self->send_cmd( "time " . $$self{'time'} ); } else { From 8ce302c63c07cfbfc31b75eaef4733e53852c5f4 Mon Sep 17 00:00:00 2001 From: Lieven Hollevoet Date: Sun, 19 Oct 2014 20:27:41 +0200 Subject: [PATCH 16/16] Added missing end-of-playlist message to parse. --- lib/SqueezeboxCLI.pm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/SqueezeboxCLI.pm b/lib/SqueezeboxCLI.pm index ea8e9a605..f4153faca 100644 --- a/lib/SqueezeboxCLI.pm +++ b/lib/SqueezeboxCLI.pm @@ -288,7 +288,7 @@ sub process_cli_response { $$self{mixer_volume} = $1; $self->debug( $$self{object_name} . " mixer volume is " . $1 ); } - if ( ( $response =~ /^pause 1/ || $response =~ /mode[:| ][pause|stop]/ ) + if ( ( $response =~ /^pause 1/ || $response =~ /mode[:| ][pause|stop]/ || $response =~ /playlist stop/ ) && $self->state() ne 'off' ) { $self->debug( $$self{object_name}