98_BOSEST: new TTS features and bugfixes

10_EQ3BT: use all BT interfaces

git-svn-id: https://svn.fhem.de/fhem/trunk@13274 2b470e98-0d58-463d-a4d8-8e2adae1ed80
This commit is contained in:
dominik
2017-01-29 17:45:23 +00:00
parent 0cf54ad8c7
commit 9cd1da33f3
3 changed files with 394 additions and 117 deletions

View File

@@ -1,5 +1,13 @@
# Add changes at the top of the list. Keep it in ASCII, and 80-char wide. # Add changes at the top of the list. Keep it in ASCII, and 80-char wide.
# Do not insert empty lines here, update check depends on it. # Do not insert empty lines here, update check depends on it.
- feature: 98_BOSEST: NEW REQUIREMENT sox, libsox-fmt-mp3 for TTS
- feature: 98_BOSEST: support more than 100 chars for TTS
- bugfix: 98_BOSEST: several TTS and Spotify bugfixes
- feature: 98_BOSEST: support playPause toggle
- feature: 10_EQ3BT: use all available BT interfaces
- feature: 10_EQ3BT: new reading lastChangeBy FHEM/Thermostat
- feature: 10_EQ3BT: support $readingFnAttribute
- bugfix: 10_EQ3BT: do not run parallel gatttool commands for same dev
- feature: FB_CALLMONITOR: new set command "reopen" - feature: FB_CALLMONITOR: new set command "reopen"
- feature: 66_ECMD: new attribute autoReopen - feature: 66_ECMD: new attribute autoReopen
- update: 74_AMAD: Version 2.6.8 new feature sendSMS - update: 74_AMAD: Version 2.6.8 new feature sendSMS

View File

@@ -1,15 +1,30 @@
############################################################# #############################################################
# #
# EQ3BT.pm (c) by Dominik Karall, 2016 # EQ3BT.pm (c) by Dominik Karall, 2016-2017
# dominik karall at gmail dot com # dominik karall at gmail dot com
# $Id$ # $Id$
# #
# FHEM module to communicate with EQ-3 Bluetooth thermostats # FHEM module to communicate with EQ-3 Bluetooth thermostats
# #
# Version: 1.1.3 # Version: 2.0.0
# #
############################################################# #############################################################
# #
# v2.0.0 - 20170129
# - FEATURE: use all available bluetooth interfaces to communicate
# with the bluetooth thermostat
# - FEATURE: new reading bluetoothDevice (shows used hci device)
# - CHANGE: change maximum retries to 20
# - FEATURE: new set function resetErrorCounters
# - FEATURE: new set function resetConsumption (not today/yesterday)
# - FEATURE: new reading lastChangeBy FHEM or thermostat
# indicates who was responsible for the last change
# - FEATURE: support $readingFnAttributes
# - FEATURE: add VERSION internal and log output
# - CHANGE: updateStatus is now 3min intervall starting from
# last working updateStatus
# - BUGFIX: do not run parallel gatttool commands for the same device
#
# v1.1.3 - 20161211 # v1.1.3 - 20161211
# - BUGFIX: better error handling if no notification was received # - BUGFIX: better error handling if no notification was received
# - BUGFIX: update system information fixed # - BUGFIX: update system information fixed
@@ -102,6 +117,7 @@
# set tempconf 17 comfort*2 eco*2 # set tempconf 17 comfort*2 eco*2
# #
# TODOs # TODOs
# - create virtual device (wohnzimmer)
# - read/set eco/comfort temperature # - read/set eco/comfort temperature
# - read/set tempOffset # - read/set tempOffset
# - read/set windowOpen time settings # - read/set windowOpen time settings
@@ -126,6 +142,7 @@ sub EQ3BT_Initialize($) {
$hash->{GetFn} = 'EQ3BT_Get'; $hash->{GetFn} = 'EQ3BT_Get';
$hash->{SetFn} = 'EQ3BT_Set'; $hash->{SetFn} = 'EQ3BT_Set';
$hash->{AttrFn} = 'EQ3BT_Attribute'; $hash->{AttrFn} = 'EQ3BT_Attribute';
$hash->{AttrList} = $readingFnAttributes;
return undef; return undef;
} }
@@ -138,6 +155,8 @@ sub EQ3BT_Define($$) {
my $mac; my $mac;
$hash->{STATE} = "initialized"; $hash->{STATE} = "initialized";
$hash->{VERSION} = "2.0.0";
Log3 $hash, 3, "EQ3BT: EQ-3 Bluetooth Thermostat ".$hash->{VERSION};
if (int(@a) > 3) { if (int(@a) > 3) {
return 'EQ3BT: Wrong syntax, must be define <name> EQ3BT <mac address>'; return 'EQ3BT: Wrong syntax, must be define <name> EQ3BT <mac address>';
@@ -146,15 +165,32 @@ sub EQ3BT_Define($$) {
$hash->{MAC} = $a[2]; $hash->{MAC} = $a[2];
} }
EQ3BT_updateHciDevicelist($hash);
BlockingCall("EQ3BT_pairDevice", $name."|".$hash->{MAC}); BlockingCall("EQ3BT_pairDevice", $name."|".$hash->{MAC});
RemoveInternalTimer($hash); RemoveInternalTimer($hash);
InternalTimer(gettimeofday()+60, "EQ3BT_updateStatusWithTimer", $hash, 0); InternalTimer(gettimeofday()+60, "EQ3BT_updateStatus", $hash, 0);
InternalTimer(gettimeofday()+20, "EQ3BT_updateSystemInformationWithTimer", $hash, 0); InternalTimer(gettimeofday()+20, "EQ3BT_updateSystemInformation", $hash, 0);
return undef; return undef;
} }
sub EQ3BT_updateHciDevicelist {
my ($hash) = @_;
#check for hciX devices
$hash->{helper}{hcidevices} = ();
my @btDevices = split("\n", qx(hcitool dev));
foreach my $btDevLine (@btDevices) {
if($btDevLine =~ /hci(.)/) {
push(@{$hash->{helper}{hcidevices}}, $1);
}
}
$hash->{helper}{currenthcidevice} = 0;
readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1);
return undef;
}
sub EQ3BT_pairDevice { sub EQ3BT_pairDevice {
my ($string) = @_; my ($string) = @_;
my ($name, $mac) = split("\\|", $string); my ($name, $mac) = split("\\|", $string);
@@ -184,8 +220,8 @@ sub EQ3BT_Set($@) {
# #
my ($hash, $name, @params) = @_; my ($hash, $name, @params) = @_;
my $workType = shift(@params); my $workType = shift(@params);
my $list = "desiredTemperature:slider,4.5,0.5,29.5,1 updateStatus:noArg boost:on,off mode:manual,automatic eco:noArg comfort:noArg"; my $list = "desiredTemperature:slider,4.5,0.5,29.5,1 updateStatus:noArg boost:on,off mode:manual,automatic eco:noArg comfort:noArg ".
#my $list = "desiredTemperature:slider,5,0.5,30,1 boost daymode nightmode childlock holidaymode datetime window program"; "resetErrorCounters:noArg resetConsumption:noArg";
# check parameters for set function # check parameters for set function
if($workType eq "?") { if($workType eq "?") {
@@ -209,6 +245,10 @@ sub EQ3BT_Set($@) {
EQ3BT_setEco($hash); EQ3BT_setEco($hash);
} elsif($workType eq "comfort") { } elsif($workType eq "comfort") {
EQ3BT_setComfort($hash); EQ3BT_setComfort($hash);
} elsif($workType eq "resetErrorCounters") {
EQ3BT_setResetErrorCounters($hash);
} elsif($workType eq "resetConsumption") {
EQ3BT_setResetConsumption($hash);
} elsif($workType eq "childlock") { } elsif($workType eq "childlock") {
return "EQ3BT: childlock requires on/off as additional parameter" if(int(@params) < 1); return "EQ3BT: childlock requires on/off as additional parameter" if(int(@params) < 1);
EQ3BT_setChildlock($hash, $params[0]); EQ3BT_setChildlock($hash, $params[0]);
@@ -230,6 +270,26 @@ sub EQ3BT_Set($@) {
return undef; return undef;
} }
### resetErrorCounters ###
sub EQ3BT_setResetErrorCounters {
my ($hash) = @_;
foreach my $reading (keys %{ $hash->{READINGS} }) {
if($reading =~ /errorCount-.*/) {
readingsSingleUpdate($hash, $reading, 0, 1);
}
}
return undef;
}
### resetConsumption ###
sub EQ3BT_setResetConsumption {
my ($hash) = @_;
readingsSingleUpdate($hash, "consumption", 0, 1);
return undef;
}
### updateSystemInformation ### ### updateSystemInformation ###
sub EQ3BT_updateSystemInformation { sub EQ3BT_updateSystemInformation {
my ($hash) = @_; my ($hash) = @_;
@@ -237,16 +297,9 @@ sub EQ3BT_updateSystemInformation {
$hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|updateSystemInformation|0x0411|00|listen", "EQ3BT_processGatttoolResult", 300, "EQ3BT_killGatttool", $hash); $hash->{helper}{RUNNING_PID} = BlockingCall("EQ3BT_execGatttool", $name."|".$hash->{MAC}."|updateSystemInformation|0x0411|00|listen", "EQ3BT_processGatttoolResult", 300, "EQ3BT_killGatttool", $hash);
} }
sub EQ3BT_updateSystemInformationWithTimer {
my ($hash) = @_;
EQ3BT_updateSystemInformation($hash);
InternalTimer(gettimeofday()+7200+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
return undef;
}
sub EQ3BT_updateSystemInformationSuccessful { sub EQ3BT_updateSystemInformationSuccessful {
my ($hash, $handle, $value) = @_; my ($hash, $handle, $value) = @_;
InternalTimer(gettimeofday()+7200+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
return undef; return undef;
} }
@@ -256,15 +309,13 @@ sub EQ3BT_updateSystemInformationRetry {
return undef; return undef;
} }
### updateStatus ### sub EQ3BT_updateSystemInformationFailed {
sub EQ3BT_updateStatusWithTimer {
my ($hash) = @_; my ($hash) = @_;
InternalTimer(gettimeofday()+7000+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
EQ3BT_updateStatus($hash); return undef;
InternalTimer(gettimeofday()+160+int(rand(20)), "EQ3BT_updateStatusWithTimer", $hash, 0);
} }
### updateStatus ###
sub EQ3BT_updateStatus { sub EQ3BT_updateStatus {
my ($hash) = @_; my ($hash) = @_;
my $name = $hash->{NAME}; my $name = $hash->{NAME};
@@ -273,7 +324,7 @@ sub EQ3BT_updateStatus {
sub EQ3BT_updateStatusSuccessful { sub EQ3BT_updateStatusSuccessful {
my ($hash, $handle, $value) = @_; my ($hash, $handle, $value) = @_;
InternalTimer(gettimeofday()+140+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
return undef; return undef;
} }
@@ -283,6 +334,12 @@ sub EQ3BT_updateStatusRetry {
return undef; return undef;
} }
sub EQ3BT_updateStatusFailed {
my ($hash, $handle, $value) = @_;
InternalTimer(gettimeofday()+170+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
return undef;
}
### setDesiredTemperature ### ### setDesiredTemperature ###
sub EQ3BT_setDesiredTemperature($$) { sub EQ3BT_setDesiredTemperature($$) {
my ($hash, $desiredTemp) = @_; my ($hash, $desiredTemp) = @_;
@@ -297,7 +354,7 @@ sub EQ3BT_setDesiredTemperature($$) {
sub EQ3BT_setDesiredTemperatureSuccessful { sub EQ3BT_setDesiredTemperatureSuccessful {
my ($hash, $handle, $tempVal) = @_; my ($hash, $handle, $tempVal) = @_;
my $temp = (hex($tempVal) - 0x4100) / 2; my $temp = (hex($tempVal) - 0x4100) / 2;
readingsSingleUpdate($hash, "desiredTemperature", $temp, 1); readingsSingleUpdate($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
return undef; return undef;
} }
@@ -407,6 +464,7 @@ sub EQ3BT_execGatttool($) {
my ($string) = @_; my ($string) = @_;
my ($name, $mac, $workType, $handle, $value, $listen) = split("\\|", $string); my ($name, $mac, $workType, $handle, $value, $listen) = split("\\|", $string);
my $wait = 1; my $wait = 1;
my $hash = $main::defs{$name};
my $gatttool = qx(which gatttool); my $gatttool = qx(which gatttool);
chomp $gatttool; chomp $gatttool;
@@ -415,10 +473,10 @@ sub EQ3BT_execGatttool($) {
my $gtResult; my $gtResult;
while($wait) { while($wait) {
my $grepGatttool = qx(ps ax| grep \'hcitool\' | grep -v grep); my $grepGatttool = qx(ps ax| grep -E \'gatttool -b $mac\' | grep -v grep);
if(not $grepGatttool =~ /^\s*$/) { if(not $grepGatttool =~ /^\s*$/) {
#another gattool is running #another gattool is running
Log3 $name, 5, "EQ3BT ($name): another hcitool process is running. waiting..."; Log3 $name, 5, "EQ3BT ($name): another gatttool process is running. waiting...";
sleep(1); sleep(1);
} else { } else {
$wait = 0; $wait = 0;
@@ -431,9 +489,10 @@ sub EQ3BT_execGatttool($) {
$value .= $currentDate; $value .= $currentDate;
} }
my $cmd = "gatttool -b $mac --char-write-req --handle=$handle --value=$value"; my $hciDevice = "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}];
my $cmd = "gatttool -b $mac -i $hciDevice --char-write-req --handle=$handle --value=$value";
if(defined($listen) && $listen eq "listen") { if(defined($listen) && $listen eq "listen") {
$cmd = "timeout 5 ".$cmd." --listen"; $cmd = "timeout 15 ".$cmd." --listen";
} }
#redirect stderr to stdout #redirect stderr to stdout
@@ -478,6 +537,8 @@ sub EQ3BT_processGatttoolResult($) {
my $value = $a[5]; my $value = $a[5];
my $notification = $a[6]; my $notification = $a[6];
delete($hash->{helper}{RUNNING_PID});
Log3 $hash, 5, "EQ3BT ($name): gatttool return string: $string"; Log3 $hash, 5, "EQ3BT ($name): gatttool return string: $string";
$hash->{helper}{"handle$workType"} = $handle; $hash->{helper}{"handle$workType"} = $handle;
@@ -489,11 +550,15 @@ sub EQ3BT_processGatttoolResult($) {
if(defined($notification)) { if(defined($notification)) {
EQ3BT_processNotification($hash, $notification); EQ3BT_processNotification($hash, $notification);
} }
if($workType =~ /set.*/) {
readingsSingleUpdate($hash, "lastChangeBy", "FHEM", 1);
}
#call WorkTypeSuccessful function #call WorkTypeSuccessful function
my $call = "EQ3BT_".$workType."Successful"; my $call = "EQ3BT_".$workType."Successful";
#FIXME otherwise temperature is not set after successfull write
no strict "refs"; no strict "refs";
&{$call}($hash, $handle, $value); eval {
&{$call}($hash, $handle, $value);
};
use strict "refs"; use strict "refs";
RemoveInternalTimer($hash, "EQ3BT_".$workType."Retry"); RemoveInternalTimer($hash, "EQ3BT_".$workType."Retry");
$hash->{helper}{"retryCounter$workType"} = 0; $hash->{helper}{"retryCounter$workType"} = 0;
@@ -501,13 +566,38 @@ sub EQ3BT_processGatttoolResult($) {
$hash->{helper}{"retryCounter$workType"} = 0 if(!defined($hash->{helper}{"retryCounter$workType"})); $hash->{helper}{"retryCounter$workType"} = 0 if(!defined($hash->{helper}{"retryCounter$workType"}));
$hash->{helper}{"retryCounter$workType"}++; $hash->{helper}{"retryCounter$workType"}++;
Log3 $hash, 4, "EQ3BT ($name): $workType failed ($handle, $value, $notification)"; Log3 $hash, 4, "EQ3BT ($name): $workType failed ($handle, $value, $notification)";
if ($hash->{helper}{"retryCounter$workType"} > 30) { if ($hash->{helper}{"retryCounter$workType"} > 20) {
my $errorCount = ReadingsVal($hash->{NAME}, "errorCount-$workType", 0); my $errorCount = ReadingsVal($hash->{NAME}, "errorCount-$workType", 0);
readingsSingleUpdate($hash, "errorCount-$workType", $errorCount+1, 1); readingsSingleUpdate($hash, "errorCount-$workType", $errorCount+1, 1);
Log3 $hash, 3, "EQ3BT ($name): $workType, $handle, $value failed 30 times."; Log3 $hash, 3, "EQ3BT ($name): $workType, $handle, $value failed 20 times.";
$hash->{helper}{"retryCounter$workType"} = 0; $hash->{helper}{"retryCounter$workType"} = 0;
$hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0;
#call WorkTypeFailed function
my $call = "EQ3BT_".$workType."Failed";
no strict "refs";
eval {
&{$call}($hash, $handle, $value);
};
use strict "refs";
#update hci devicelist
EQ3BT_updateHciDevicelist($hash);
} else { } else {
InternalTimer(gettimeofday()+5, "EQ3BT_".$workType."Retry", $hash, 0); $hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0 if(!defined($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}}));
$hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}}++;
if ($hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} > 7) {
#reset error counter
$hash->{helper}{"retryCounterHci".$hash->{helper}{currenthcidevice}} = 0;
#use next hci device next time
$hash->{helper}{currenthcidevice} += 1;
my $maxHciDevices = @{ $hash->{helper}{hcidevices} } - 1;
if($hash->{helper}{currenthcidevice} > $maxHciDevices) {
$hash->{helper}{currenthcidevice} = 0;
}
#update reading
readingsSingleUpdate($hash, "bluetoothDevice", "hci".$hash->{helper}{hcidevices}[$hash->{helper}{currenthcidevice}], 1);
}
InternalTimer(gettimeofday()+3+int(rand(5)), "EQ3BT_".$workType."Retry", $hash, 0);
} }
} }
@@ -558,31 +648,44 @@ sub EQ3BT_processNotification {
my $consumptionTodaySecSinceLastChange = ReadingsAge($hash->{NAME}, "consumptionToday", 0); my $consumptionTodaySecSinceLastChange = ReadingsAge($hash->{NAME}, "consumptionToday", 0);
my $oldVal = ReadingsVal($hash->{NAME}, "valvePosition", 0); my $oldVal = ReadingsVal($hash->{NAME}, "valvePosition", 0);
my $consumptionDiff = 0; my $consumptionDiff = 0;
if($timeSinceLastChange < 300) { if($timeSinceLastChange < 600) {
$consumptionDiff += ($oldVal + $pct) / 2 * $timeSinceLastChange / 3600; $consumptionDiff += ($oldVal + $pct) / 2 * $timeSinceLastChange / 3600;
} }
EQ3BT_readingsSingleUpdateIfChanged($hash, "consumption", sprintf("%.3f", $consumption+$consumptionDiff));
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time); my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
if($consumptionTodaySecSinceLastChange > ($hour*3600+$min*60+$sec)) { if($consumptionTodaySecSinceLastChange > ($hour*3600+$min*60+$sec)) {
readingsSingleUpdate($hash, "consumptionYesterday", $consumptionToday + $consumptionDiff/2, 1); readingsSingleUpdate($hash, "consumptionYesterday", $consumptionToday + $consumptionDiff/2, 1);
readingsSingleUpdate($hash, "consumptionToday", 0 + $consumptionDiff/2, 1); readingsSingleUpdate($hash, "consumptionToday", 0 + $consumptionDiff/2, 1);
} else { } else {
readingsSingleUpdate($hash, "consumptionToday", sprintf("%.3f", $consumptionToday+$consumptionDiff), 1); EQ3BT_readingsSingleUpdateIfChanged($hash, "consumptionToday", sprintf("%.3f", $consumptionToday+$consumptionDiff));
} }
readingsSingleUpdate($hash, "valvePosition", $pct, 1);
#changes below this line will set lastchangeby
readingsSingleUpdate($hash, "windowOpen", $wndOpen, 1); readingsSingleUpdate($hash, "windowOpen", $wndOpen, 1);
readingsSingleUpdate($hash, "ecoMode", $eco, 1); readingsSingleUpdate($hash, "ecoMode", $eco, 1);
readingsSingleUpdate($hash, "battery", $batteryStr, 1); readingsSingleUpdate($hash, "battery", $batteryStr, 1);
readingsSingleUpdate($hash, "boost", $isBoost, 1); readingsSingleUpdate($hash, "boost", $isBoost, 1);
readingsSingleUpdate($hash, "consumption", sprintf("%.3f", $consumption+$consumptionDiff), 1);
readingsSingleUpdate($hash, "mode", $modeStr, 1); readingsSingleUpdate($hash, "mode", $modeStr, 1);
readingsSingleUpdate($hash, "valvePosition", $pct, 1); readingsSingleUpdate($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
readingsSingleUpdate($hash, "desiredTemperature", $temp, 1);
} }
return undef; return undef;
} }
sub EQ3BT_readingsSingleUpdateIfChanged {
my ($hash, $reading, $value, $setLastChange) = @_;
my $curVal = ReadingsVal($hash->{NAME}, $reading, "");
if($curVal ne $value) {
readingsSingleUpdate($hash, $reading, $value, 1);
if(defined($setLastChange)) {
readingsSingleUpdate($hash, "lastChangeBy", "Thermostat", 1);
}
}
}
sub EQ3BT_killGatttool($) { sub EQ3BT_killGatttool($) {
} }

View File

@@ -1,16 +1,36 @@
############################################################# #############################################################
# #
# BOSEST.pm (c) by Dominik Karall, 2016 # BOSEST.pm (c) by Dominik Karall, 2016-2017
# dominik karall at gmail dot com # dominik karall at gmail dot com
# $Id$ # $Id$
# #
# FHEM module to communicate with BOSE SoundTouch system # FHEM module to communicate with BOSE SoundTouch system
# API as defined in BOSE SoundTouchAPI_WebServices_v1.0.1.pdf # API as defined in BOSE SoundTouchAPI_WebServices_v1.0.1.pdf
# #
# Version: 2.0.1 # Version: 2.1.0
# #
############################################################# #############################################################
# #
# v2.1.0 - 20170129
# - NEW REQUIREMENT: TTS: sox, libsox-fmt-mp3 (only required for TTS)
# - FEATURE: TTS: add 1 second silence before TTS message for speak to
# prevent low volume on first words
# - FEATURE: TTS: support "unlimited" characters in TTS speak.
# Text is split in sentences which are afterwards
# merged with sox. Same sentences are downloaded
# only once a month to reduce requests to Google.
# - FEATURE: TTS: remove ttsDlnaServer attribut, it will be automatically discovered
# - BUGFIX: TTS: support pause/stop after speak if previous state was paused/stopped
# - BUGFIX: TTS: fix resume after speak when spotify running
# - BUGFIX: TTS: fix speakChannel for spotify presets
# - BUGFIX: TTS: use pause on TTS instead of stop to allow proper resume
# - BUGFIX: TTS: improved check after TTS play to restore previous state
# - BUGFIX: TTS: if state was invalid before TTS it will be set to standby
# - BUGFIX: fix save spotify to channel_7-20
# - BUGFIX: fix list of arguments
# - FEATURE: add $readingFnAttributes
# - FEATURE: add playPause toggle command
#
# v2.0.1 - 20161203 # v2.0.1 - 20161203
# - FEATURE: support shuffle/repeat (thx@rockyou) # - FEATURE: support shuffle/repeat (thx@rockyou)
# - BUGFIX: support special characters for TTS (thx@hschuett) # - BUGFIX: support special characters for TTS (thx@hschuett)
@@ -201,12 +221,7 @@
# - change preset via /key # - change preset via /key
# #
# TODO # TODO
# - redesign multiroom functionality (virtual devices: represent the readings of master device # - set title/album/artist for TTS files (--comment "Title=Title..")
# and send the commands only to the master device (except volume?)
# automatically create group before playing
# - support multiroom volume (check with SoundTouch app to see commands)
# - use websocket frame ping (WS_PING) instead of websocket XML ping
# - TTS code cleanup (group functions logically)
# - check if Mojolicious should be used for HTTPGET/HTTPPOST # - check if Mojolicious should be used for HTTPGET/HTTPPOST
# - ramp up/down volume support in SetExtensions # - ramp up/down volume support in SetExtensions
# #
@@ -238,6 +253,8 @@ use URI::Escape;
my $BOSEST_GOOGLE_NOT_AVAILABLE_TEXT = "Hello, I'm sorry, but Google Translate is currently not available."; my $BOSEST_GOOGLE_NOT_AVAILABLE_TEXT = "Hello, I'm sorry, but Google Translate is currently not available.";
my $BOSEST_GOOGLE_NOT_AVAILABLE_LANG = "en"; my $BOSEST_GOOGLE_NOT_AVAILABLE_LANG = "en";
my $BOSEST_READ_CMDREF_TEXT = "Hello, I'm sorry, but you need to install new libraries, please read command reference.";
my $BOSEST_READ_CMDREF_LANG = "en";
sub BOSEST_Initialize($) { sub BOSEST_Initialize($) {
my ($hash) = @_; my ($hash) = @_;
@@ -247,6 +264,7 @@ sub BOSEST_Initialize($) {
$hash->{GetFn} = 'BOSEST_Get'; $hash->{GetFn} = 'BOSEST_Get';
$hash->{SetFn} = 'BOSEST_Set'; $hash->{SetFn} = 'BOSEST_Set';
$hash->{AttrFn} = 'BOSEST_Attribute'; $hash->{AttrFn} = 'BOSEST_Attribute';
$hash->{AttrList} = $readingFnAttributes;
return undef; return undef;
} }
@@ -281,6 +299,7 @@ sub BOSEST_Define($$) {
#init statecheck #init statecheck
$hash->{helper}{stateCheck}{enabled} = 0; $hash->{helper}{stateCheck}{enabled} = 0;
$hash->{helper}{stateCheck}{actionActive} = 0;
#init switchSource #init switchSource
$hash->{helper}{switchSource} = ""; $hash->{helper}{switchSource} = "";
@@ -288,11 +307,14 @@ sub BOSEST_Define($$) {
#init speak channel functionality #init speak channel functionality
$hash->{helper}{lastSpokenChannel} = ""; $hash->{helper}{lastSpokenChannel} = "";
foreach my $attrname (qw(channel_07 channel_08 channel_09 channel_10 channel_11 my $attrList = "channel_07 channel_08 channel_09 channel_10 channel_11 ".
channel_12 channel_13 channel_14 channel_15 channel_16 "channel_12 channel_13 channel_14 channel_15 channel_16 ".
channel_17 channel_18 channel_19 channel_20 ignoreDeviceIDs "channel_17 channel_18 channel_19 channel_20 ignoreDeviceIDs ".
ttsDirectory ttsLanguage ttsSpeakOnError ttsDLNAServer ttsVolume "ttsDirectory ttsLanguage ttsSpeakOnError ttsVolume ".
speakChannel autoZone)) { "speakChannel autoZone";
my @attrListArr = split(" ", $attrList);
foreach my $attrname (@attrListArr) {
addToDevAttrList($name, $attrname); addToDevAttrList($name, $attrname);
} }
@@ -309,7 +331,7 @@ sub BOSEST_Define($$) {
$hash->{helper}{supportedBassCmds} = ""; $hash->{helper}{supportedBassCmds} = "";
if (int(@a) < 3) { if (int(@a) < 3) {
Log3 $hash, 3, "BOSEST: BOSE SoundTouch v2.0.1"; Log3 $hash, 3, "BOSEST: BOSE SoundTouch v2.1.0";
#start discovery process 30s delayed #start discovery process 30s delayed
InternalTimer(gettimeofday()+30, "BOSEST_startDiscoveryProcess", $hash, 0); InternalTimer(gettimeofday()+30, "BOSEST_startDiscoveryProcess", $hash, 0);
@@ -331,8 +353,6 @@ sub BOSEST_Attribute($$$$) {
return "BOSEST: wrong format" if(!defined($value[2])); return "BOSEST: wrong format" if(!defined($value[2]));
#update reading for channel_X #update reading for channel_X
readingsSingleUpdate($main::defs{$devName}, $attrName, $value[0], 1); readingsSingleUpdate($main::defs{$devName}, $attrName, $value[0], 1);
} elsif($attrName eq "ttsDLNAServer") {
BOSEST_addDLNAServer($main::defs{$devName}, $attrValue);
} }
} elsif($mode eq "del") { } elsif($mode eq "del") {
if(substr($attrName, 0, 8) eq "channel_") { if(substr($attrName, 0, 8) eq "channel_") {
@@ -356,18 +376,18 @@ sub BOSEST_Set($@) {
} }
@params = @params2; @params = @params2;
my $list = "on:noArg off:noArg power:noArg play:noArg my $list = "on:noArg off:noArg power:noArg play:noArg ".
mute:on,off,toggle recent source:".$hash->{helper}{supportedSourcesCmds}." "playPause:noArg ".
shuffle:on,off "mute:on,off,toggle recent source:".$hash->{helper}{supportedSourcesCmds}.
repeat:all,one,off "shuffle:on,off repeat:all,one,off ".
nextTrack:noArg prevTrack:noArg playTrack speak speakOff "nextTrack:noArg prevTrack:noArg playTrack speak speakOff ".
playEverywhere:noArg stopPlayEverywhere:noArg createZone addToZone removeFromZone "playEverywhere:noArg stopPlayEverywhere:noArg createZone addToZone removeFromZone ".
clock:enable,disable "clock:enable,disable ".
stop:noArg pause:noArg channel:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 "stop:noArg pause:noArg channel:1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 ".
volume:slider,0,1,100 ".$hash->{helper}{supportedBassCmds}." "volume:slider,0,1,100 ".$hash->{helper}{supportedBassCmds}." ".
saveChannel:07,08,09,10,11,12,13,14,15,16,17,18,19,20 "saveChannel:07,08,09,10,11,12,13,14,15,16,17,18,19,20 ".
addDLNAServer:".$hash->{helper}{dlnaServers}." "addDLNAServer:".$hash->{helper}{dlnaServers}." ".
removeDLNAServer:".ReadingsVal($hash->{NAME}, "connectedDLNAServers", "noArg"); "removeDLNAServer:".ReadingsVal($hash->{NAME}, "connectedDLNAServers", "noArg");
# check parameters for set function # check parameters for set function
#DEVELOPNEWFUNCTION-1 #DEVELOPNEWFUNCTION-1
@@ -441,6 +461,8 @@ sub BOSEST_Set($@) {
BOSEST_stop($hash); BOSEST_stop($hash);
} elsif($workType eq "pause") { } elsif($workType eq "pause") {
BOSEST_pause($hash); BOSEST_pause($hash);
} elsif($workType eq "playPause") {
BOSEST_playPause($hash);
} elsif($workType eq "power") { } elsif($workType eq "power") {
BOSEST_power($hash); BOSEST_power($hash);
} elsif($workType eq "on") { } elsif($workType eq "on") {
@@ -459,7 +481,6 @@ sub BOSEST_Set($@) {
} elsif($workType eq "speak" or $workType eq "speakOff") { } elsif($workType eq "speak" or $workType eq "speakOff") {
return "BOSEST: speak requires quoted text as additional parameters" if(int(@params) < 1); return "BOSEST: speak requires quoted text as additional parameters" if(int(@params) < 1);
return "BOSEST: speak requires quoted text" if(substr($blankParams, 0, 1) ne "\""); return "BOSEST: speak requires quoted text" if(substr($blankParams, 0, 1) ne "\"");
return "BOSEST: speak maximum text length is 100 characters" if(length($params[0])>100);
if(AttrVal($hash->{NAME}, "ttsDirectory", "") eq "") { if(AttrVal($hash->{NAME}, "ttsDirectory", "") eq "") {
return "BOSEST: Please set ttsDirectory attribute first. return "BOSEST: Please set ttsDirectory attribute first.
FHEM user needs permissions to write to that directory. FHEM user needs permissions to write to that directory.
@@ -586,17 +607,18 @@ sub BOSEST_removeDLNAServer($$) {
sub BOSEST_saveChannel($$) { sub BOSEST_saveChannel($$) {
my ($hash, $channel) = @_; my ($hash, $channel) = @_;
if(ReadingsVal($hash->{NAME}, "state", "stopped") ne "playing") { if(ReadingsVal($hash->{NAME}, "contentItemLocation", "") eq "") {
return "BOSEST: No playing channel. Start a channel and save afterwards."; return "BOSEST: No active channel.";
} }
#itemname, location, source, sourceaccount #itemname, type, location, source, sourceaccount
my $itemName = ReadingsVal($hash->{NAME}, "contentItemItemName", ""); my $itemName = ReadingsVal($hash->{NAME}, "contentItemItemName", "");
my $location = ReadingsVal($hash->{NAME}, "contentItemLocation", ""); my $location = ReadingsVal($hash->{NAME}, "contentItemLocation", "");
my $type = ReadingsVal($hash->{NAME}, "contentItemType", "");
my $source = ReadingsVal($hash->{NAME}, "contentItemSource", ""); my $source = ReadingsVal($hash->{NAME}, "contentItemSource", "");
my $sourceAccount = ReadingsVal($hash->{NAME}, "contentItemSourceAccount", ""); my $sourceAccount = ReadingsVal($hash->{NAME}, "contentItemSourceAccount", "");
fhem("attr $hash->{NAME} channel_$channel $itemName|$location|$source|$sourceAccount"); fhem("attr $hash->{NAME} channel_$channel $itemName|$type|$location|$source|$sourceAccount");
return undef; return undef;
} }
@@ -749,6 +771,7 @@ sub BOSEST_setRecent($$) {
BOSEST_setContentItem($hash, BOSEST_setContentItem($hash,
$hash->{helper}{recents}{$nr}{itemName}, $hash->{helper}{recents}{$nr}{itemName},
$hash->{helper}{recents}{$nr}{type},
$hash->{helper}{recents}{$nr}{location}, $hash->{helper}{recents}{$nr}{location},
$hash->{helper}{recents}{$nr}{source}, $hash->{helper}{recents}{$nr}{source},
$hash->{helper}{recents}{$nr}{sourceAccount}); $hash->{helper}{recents}{$nr}{sourceAccount});
@@ -756,8 +779,10 @@ sub BOSEST_setRecent($$) {
return undef; return undef;
} }
sub BOSEST_setContentItem($$$$$) { sub BOSEST_setContentItem {
my ($hash, $itemName, $location, $source, $sourceAccount) = @_; my ($hash, $itemName, $type, $location, $source, $sourceAccount) = @_;
$type = "" if(!defined($type));
my $postXml = "<ContentItem source=\"". my $postXml = "<ContentItem source=\"".
$source. $source.
@@ -765,6 +790,8 @@ sub BOSEST_setContentItem($$$$$) {
$sourceAccount. $sourceAccount.
"\" location=\"". "\" location=\"".
$location. $location.
"\" type=\"".
$type.
"\">". "\">".
"<itemName>". "<itemName>".
$itemName. $itemName.
@@ -877,10 +904,13 @@ sub BOSEST_setPreset($$) {
my $channelVal = AttrVal($hash->{NAME}, sprintf("channel_%02d", $preset), "0"); my $channelVal = AttrVal($hash->{NAME}, sprintf("channel_%02d", $preset), "0");
return undef if($channelVal eq "0"); return undef if($channelVal eq "0");
my @channel = split("\\|", $channelVal); my @channel = split("\\|", $channelVal);
$channel[1] = "" if(!defined($channel[1]));
$channel[2] = "" if(!defined($channel[2]));
$channel[3] = "" if(!defined($channel[3])); $channel[3] = "" if(!defined($channel[3]));
Log3 $hash, 5, "BOSEST: AttrVal: $channel[0], $channel[1], $channel[2], $channel[3]"; $channel[4] = "" if(!defined($channel[4]));
Log3 $hash, 5, "BOSEST: AttrVal: $channel[0], $channel[1], $channel[2], $channel[3], $channel[4]";
#format: itemName|location|source|sourceAccount #format: itemName|location|source|sourceAccount
BOSEST_setContentItem($hash, $channel[0], $channel[1], $channel[2], $channel[3]); BOSEST_setContentItem($hash, $channel[0], $channel[1], $channel[2], $channel[3], $channel[4]);
} }
return undef; return undef;
} }
@@ -891,6 +921,12 @@ sub BOSEST_play($) {
return undef; return undef;
} }
sub BOSEST_playPause($) {
my ($hash) = @_;
BOSEST_sendKey($hash, "PLAY_PAUSE");
return undef;
}
sub BOSEST_stop($) { sub BOSEST_stop($) {
my ($hash) = @_; my ($hash) = @_;
BOSEST_sendKey($hash, "STOP"); BOSEST_sendKey($hash, "STOP");
@@ -968,6 +1004,13 @@ sub BOSEST_speak($$$$$) {
$lang = AttrVal($hash->{NAME}, "ttsLanguage", "en") if($lang eq ""); $lang = AttrVal($hash->{NAME}, "ttsLanguage", "en") if($lang eq "");
$volume = AttrVal($hash->{NAME}, "ttsVolume", ReadingsVal($hash->{NAME}, "volume", 20)) if($volume eq ""); $volume = AttrVal($hash->{NAME}, "ttsVolume", ReadingsVal($hash->{NAME}, "volume", 20)) if($volume eq "");
my $sox = qx(which sox);
chomp $sox;
if(!-x $sox) {
BOSEST_playGoogleTTS($hash, $ttsDir, $BOSEST_READ_CMDREF_TEXT, $volume, $BOSEST_READ_CMDREF_LANG, $stopAfterSpeak);
return undef;
}
#download file and play #download file and play
BOSEST_playGoogleTTS($hash, $ttsDir, $text, $volume, $lang, $stopAfterSpeak); BOSEST_playGoogleTTS($hash, $ttsDir, $text, $volume, $lang, $stopAfterSpeak);
@@ -980,7 +1023,9 @@ sub BOSEST_saveCurrentState($) {
$hash->{helper}{savedState}{volume} = ReadingsVal($hash->{NAME}, "volume", 20); $hash->{helper}{savedState}{volume} = ReadingsVal($hash->{NAME}, "volume", 20);
$hash->{helper}{savedState}{source} = ReadingsVal($hash->{NAME}, "source", ""); $hash->{helper}{savedState}{source} = ReadingsVal($hash->{NAME}, "source", "");
$hash->{helper}{savedState}{bass} = ReadingsVal($hash->{NAME}, "bass", ""); $hash->{helper}{savedState}{bass} = ReadingsVal($hash->{NAME}, "bass", "");
$hash->{helper}{savedState}{playStatus} = ReadingsVal($hash->{NAME}, "playStatus", "STOP_STATE");
$hash->{helper}{savedState}{contentItemItemName} = ReadingsVal($hash->{NAME}, "contentItemItemName", ""); $hash->{helper}{savedState}{contentItemItemName} = ReadingsVal($hash->{NAME}, "contentItemItemName", "");
$hash->{helper}{savedState}{contentItemType} = ReadingsVal($hash->{NAME}, "contentItemType", "");
$hash->{helper}{savedState}{contentItemLocation} = ReadingsVal($hash->{NAME}, "contentItemLocation", ""); $hash->{helper}{savedState}{contentItemLocation} = ReadingsVal($hash->{NAME}, "contentItemLocation", "");
$hash->{helper}{savedState}{contentItemSource} = ReadingsVal($hash->{NAME}, "contentItemSource", ""); $hash->{helper}{savedState}{contentItemSource} = ReadingsVal($hash->{NAME}, "contentItemSource", "");
$hash->{helper}{savedState}{contentItemSourceAccount} = ReadingsVal($hash->{NAME}, "contentItemSourceAccount", ""); $hash->{helper}{savedState}{contentItemSourceAccount} = ReadingsVal($hash->{NAME}, "contentItemSourceAccount", "");
@@ -995,13 +1040,20 @@ sub BOSEST_restoreSavedState($) {
BOSEST_setBass($hash, $hash->{helper}{savedState}{bass}); BOSEST_setBass($hash, $hash->{helper}{savedState}{bass});
#bose off when source was off #bose off when source was off
if($hash->{helper}{savedState}{source} eq "STANDBY") { if($hash->{helper}{savedState}{source} eq "STANDBY" or $hash->{helper}{savedState}{source} eq "INVALID_SOURCE") {
BOSEST_off($hash); BOSEST_off($hash);
} else { } else {
BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName}, BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName},
$hash->{helper}{savedState}{contentItemType},
$hash->{helper}{savedState}{contentItemLocation}, $hash->{helper}{savedState}{contentItemLocation},
$hash->{helper}{savedState}{contentItemSource}, $hash->{helper}{savedState}{contentItemSource},
$hash->{helper}{savedState}{contentItemSourceAccount}); $hash->{helper}{savedState}{contentItemSourceAccount});
if($hash->{helper}{savedState}{playStatus} eq "STOP_STATE") {
InternalTimer(gettimeofday()+0.8, "BOSEST_stop", $hash, 0);
} elsif($hash->{helper}{savedState}{playStatus} eq "PAUSE_STATE") {
InternalTimer(gettimeofday()+0.8, "BOSEST_pause", $hash, 0);
}
} }
return undef; return undef;
@@ -1014,6 +1066,7 @@ sub BOSEST_restoreVolumeAndOff($) {
BOSEST_setBass($hash, $hash->{helper}{savedState}{bass}); BOSEST_setBass($hash, $hash->{helper}{savedState}{bass});
BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName}, BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName},
$hash->{helper}{savedState}{contentItemType},
$hash->{helper}{savedState}{contentItemLocation}, $hash->{helper}{savedState}{contentItemLocation},
$hash->{helper}{savedState}{contentItemSource}, $hash->{helper}{savedState}{contentItemSource},
$hash->{helper}{savedState}{contentItemSourceAccount}); $hash->{helper}{savedState}{contentItemSourceAccount});
@@ -1029,17 +1082,14 @@ sub BOSEST_downloadGoogleNotAvailable($) {
my $md5 = md5_hex($lang.$text); my $md5 = md5_hex($lang.$text);
my $filename = $ttsDir."/".$md5.".mp3"; my $filename = $ttsDir."/".$md5.".mp3";
if (-f $filename) { if (! -f $filename) {
#file exists already BOSEST_retrieveGooglTTSFile($hash, $filename, $md5, $text, $lang);
return undef;
} }
BOSEST_downloadGoogleTTS($hash, $filename, $md5, $text, $lang);
return undef; return undef;
} }
sub BOSEST_downloadGoogleTTS($$$$$;$) { sub BOSEST_retrieveGooglTTSFile($$$$$;$) {
my ($hash, $filename, $md5, $text, $lang, $callback) = @_; my ($hash, $filename, $md5, $text, $lang, $callback) = @_;
my $uri_text = uri_escape($text); my $uri_text = uri_escape($text);
@@ -1058,22 +1108,67 @@ sub BOSEST_downloadGoogleTTS($$$$$;$) {
return undef; return undef;
} }
sub BOSEST_generateSilence {
my ($hash) = @_;
my $ttsDir = AttrVal($hash->{NAME}, "ttsDirectory", "");
my $silenceFile = $ttsDir."/BOSEST_silence.mp3";
my $soxCmd;
if(!-f $silenceFile) {
#generate silence file
$soxCmd = "sox -n -r 24000 -c 1 $silenceFile trim 0.0 1";
qx($soxCmd);
}
return undef;
}
sub BOSEST_joinAudioFilesBlocking {
my ($string) = @_;
my ($name, $outputFile, @inputFiles) = split("\\|", $string);
my $ttsDir = AttrVal($name, "ttsDirectory", "");
my $hash = $main::defs{$name};
my $inputF = join(" ", map { $ttsDir."/".$_ } @inputFiles);
my $outputF = $ttsDir."/".$outputFile;
my $outputFileTmp = $ttsDir."/tmp_".$outputFile;
BOSEST_generateSilence($hash);
my $soxCmd = "sox $inputF $outputFileTmp";
Log3 $hash, 5, "SOX: $soxCmd";
my $soxRes = qx($soxCmd);
qx(mv $outputFileTmp $outputF);
return $name;
}
sub BOSEST_playMessageStringArg {
my ($name) = @_;
my $hash = $main::defs{$name};
BOSEST_playMessage($hash, "v1_".$hash->{helper}{tts}{fulltextmd5}, $hash->{helper}{tts}{volume}, $hash->{helper}{tts}{stopAfterSpeak});
return undef;
}
sub BOSEST_playMessage($$$$) { sub BOSEST_playMessage($$$$) {
my ($hash, $trackname, $volume, $stopAfterSpeak) = @_; my ($hash, $trackname, $volume, $stopAfterSpeak) = @_;
Log3 $hash, 4, "BOSEST: playMessage $trackname, $volume, $stopAfterSpeak";
BOSEST_saveCurrentState($hash); BOSEST_saveCurrentState($hash);
if($volume ne ReadingsVal($hash->{NAME}, "volume", 0)) { if($volume ne ReadingsVal($hash->{NAME}, "volume", 0)) {
BOSEST_stop($hash); BOSEST_pause($hash);
BOSEST_setVolume($hash, $volume); BOSEST_setVolume($hash, $volume);
} }
BOSEST_playTrack($hash, $trackname); BOSEST_playTrack($hash, $trackname);
$hash->{helper}{stateCheck}{enabled} = 1; $hash->{helper}{stateCheck}{enabled} = 1;
$hash->{helper}{stateCheck}{always} = 0; #after play the speaker changes contentItemItemName
#after play the speaker sets INVALID_SOURCE $hash->{helper}{stateCheck}{actionContentItemItemName} = $trackname;
$hash->{helper}{stateCheck}{actionSource} = "INVALID_SOURCE";
#check if we need to stop after speak #check if we need to stop after speak
if(defined($stopAfterSpeak) && $stopAfterSpeak eq "1") { if(defined($stopAfterSpeak) && $stopAfterSpeak eq "1") {
$hash->{helper}{stateCheck}{function} = \&BOSEST_restoreVolumeAndOff; $hash->{helper}{stateCheck}{function} = \&BOSEST_restoreVolumeAndOff;
@@ -1110,8 +1205,71 @@ sub BOSEST_deleteOldTTSFiles {
$err = setKeyValue("BOSEST_tts_files", join(",", @ttsFiles)); $err = setKeyValue("BOSEST_tts_files", join(",", @ttsFiles));
} }
sub BOSEST_playGoogleTTS($$$$$$) { sub BOSEST_playGoogleTTS {
my ($hash, $ttsDir, $text, $volume, $lang, $stopAfterSpeak) = @_; my ($hash, $ttsDir, $text, $volume, $lang, $stopAfterSpeak) = @_;
$hash->{helper}{tts}{volume} = $volume;
$hash->{helper}{tts}{stopAfterSpeak} = $stopAfterSpeak;
$hash->{helper}{tts}{fulltextmd5} = md5_hex($lang.$text);
my $filename = $ttsDir."/v1_".$hash->{helper}{tts}{fulltextmd5}.".mp3";
if(-f $filename) {
my $timestamp = (stat($filename))->mtime(); #last modification timestamp
my $now = time();
if($now-$timestamp < 2592000) {
#file is not older than 30 days
Log3 $hash, 5, "BOSEST: File $filename found. No new download required.";
BOSEST_playMessageStringArg($hash->{NAME});
return undef;
}
}
my @sentences = split (/(?<=[.?!])/, $text);
$hash->{helper}{tts}{downloads}{all} = "";
foreach my $sentence (@sentences) {
my $md5 = md5_hex($lang.$sentence);
$hash->{helper}{tts}{downloads}{$md5} = 0;
$hash->{helper}{tts}{downloads}{all} .= $md5.",";
BOSEST_downloadGoogleTTS($hash, $ttsDir, $sentence, $lang);
}
InternalTimer(gettimeofday()+1, "BOSEST_checkTTSDownloadFinished", $hash, 0);
return undef;
}
sub BOSEST_checkTTSDownloadFinished {
my ($hash) = @_;
my @allMd5 = split(",", $hash->{helper}{tts}{downloads}{all});
my $msgStatus = 1;
foreach my $md5 (@allMd5) {
if($hash->{helper}{tts}{downloads}{$md5} == 10) {
$msgStatus = 10;
} elsif($hash->{helper}{tts}{downloads}{$md5} == 0) {
$msgStatus = 0;
}
}
if($msgStatus == 10) {
if(AttrVal($hash->{NAME}, "ttsSpeakOnError", "1") eq "1") {
my $md5 = md5_hex($BOSEST_GOOGLE_NOT_AVAILABLE_LANG.$BOSEST_GOOGLE_NOT_AVAILABLE_TEXT);
BOSEST_playMessage($hash, $md5, $hash->{helper}{tts}{volume}, $hash->{helper}{tts}{stopAfterSpeak});
} else {
Log3 $hash, 3, "BOSEST: Google translate download failed.";
}
} elsif($msgStatus == 0) {
#check again in 1s
InternalTimer(gettimeofday()+1, "BOSEST_checkTTSDownloadFinished", $hash, 0);
} else {
BlockingCall("BOSEST_joinAudioFilesBlocking", $hash->{NAME}."|v1_".$hash->{helper}{tts}{fulltextmd5}.".mp3|BOSEST_silence.mp3|".join(".mp3|", @allMd5).".mp3", "BOSEST_playMessageStringArg");
}
return undef;
}
sub BOSEST_downloadGoogleTTS {
my ($hash, $ttsDir, $text, $lang) = @_;
BOSEST_downloadGoogleNotAvailable($hash); BOSEST_downloadGoogleNotAvailable($hash);
@@ -1123,13 +1281,12 @@ sub BOSEST_playGoogleTTS($$$$$$) {
my $now = time(); my $now = time();
if($now-$timestamp < 2592000) { if($now-$timestamp < 2592000) {
#file is not older than 30 days #file is not older than 30 days
#file exists, call play sub $hash->{helper}{tts}{downloads}{$md5} = 1;
BOSEST_playMessage($hash, $md5, $volume, $stopAfterSpeak);
return undef; return undef;
} }
} }
BOSEST_downloadGoogleTTS($hash, $filename, $md5, $text, $lang, sub { BOSEST_retrieveGooglTTSFile($hash, $filename, $md5, $text, $lang, sub {
my ($hash, $filename, $md5, $downloadOk) = @_; my ($hash, $filename, $md5, $downloadOk) = @_;
if($downloadOk) { if($downloadOk) {
@@ -1141,15 +1298,10 @@ sub BOSEST_playGoogleTTS($$$$$$) {
} }
$err = setKeyValue("BOSEST_tts_files", $val.$md5); $err = setKeyValue("BOSEST_tts_files", $val.$md5);
$err = setKeyValue($md5, gettimeofday()); $err = setKeyValue($md5, gettimeofday());
BOSEST_playMessage($hash, $md5, $volume, $stopAfterSpeak); $hash->{helper}{tts}{downloads}{$md5} = 1;
#add silence and play message afterwards
} else { } else {
if(AttrVal($hash->{NAME}, "ttsSpeakOnError", "1") eq "1") { $hash->{helper}{tts}{downloads}{$md5} = 10; #download error
$md5 = md5_hex($BOSEST_GOOGLE_NOT_AVAILABLE_LANG.$BOSEST_GOOGLE_NOT_AVAILABLE_TEXT);
BOSEST_playMessage($hash, $md5, $volume, $stopAfterSpeak);
} else {
Log3 $hash, 3, "BOSEST: Download Google Translate failed ($text).";
return undef;
}
} }
}); });
@@ -1185,23 +1337,35 @@ sub BOSEST_removeMusicServiceAccount($$$) {
sub BOSEST_playTrack($$) { sub BOSEST_playTrack($$) {
my ($hash, $trackName) = @_; my ($hash, $trackName) = @_;
my $ttsDlnaServer = AttrVal($hash->{NAME}, "ttsDLNAServer", ""); my $ttsDlnaServer = $hash->{helper}{ttsdlnaserver};
if(defined($ttsDlnaServer) && $ttsDlnaServer ne "") {
Log3 $hash, 4, "BOSEST: Search for $trackName on $ttsDlnaServer";
if(my $xmlTrack = BOSEST_searchTrack($hash, $ttsDlnaServer, $trackName)) {
BOSEST_setContentItem($hash,
$xmlTrack->{itemName},
$xmlTrack->{type},
$xmlTrack->{location},
$xmlTrack->{source},
$xmlTrack->{sourceAccount});
return undef;
}
}
foreach my $source (@{$hash->{helper}{sources}}) { foreach my $source (@{$hash->{helper}{sources}}) {
if($source->{source} eq "STORED_MUSIC" && $source->{status} eq "READY") { if($source->{source} eq "STORED_MUSIC" && $source->{status} eq "READY") {
#skip servers which don't equal to ttsDLNAServer attribute if set Log3 $hash, 4, "BOSEST: Search for $trackName on $source->{sourceAccount}";
if($ttsDlnaServer ne "") {
next if($ttsDlnaServer ne $source->{content});
}
Log3 $hash, 4, "BOSEST: Search for $trackName on $source->{source}";
if(my $xmlTrack = BOSEST_searchTrack($hash, $source->{sourceAccount}, $trackName)) { if(my $xmlTrack = BOSEST_searchTrack($hash, $source->{sourceAccount}, $trackName)) {
BOSEST_setContentItem($hash, BOSEST_setContentItem($hash,
$xmlTrack->{itemName}, $xmlTrack->{itemName},
$xmlTrack->{type},
$xmlTrack->{location}, $xmlTrack->{location},
$xmlTrack->{source}, $xmlTrack->{source},
$xmlTrack->{sourceAccount}); $xmlTrack->{sourceAccount});
$hash->{helper}{ttsdlnaserver} = $source->{sourceAccount};
last; last;
} }
#sleep 100ms, otherwise internal server error from BOSE speaker
select(undef, undef, undef, 0.1);
} }
} }
@@ -1213,7 +1377,7 @@ sub BOSEST_searchTrack($$$) {
my $postXml = '<search source="STORED_MUSIC" sourceAccount="'. my $postXml = '<search source="STORED_MUSIC" sourceAccount="'.
$dlnaUid. $dlnaUid.
'"><startItem>1</startItem><numItems>100</numItems><searchTerm filter="track">'. '"><startItem>1</startItem><numItems>1</numItems><searchTerm filter="track">'.
$trackName. $trackName.
'</searchTerm></search>'; '</searchTerm></search>';
@@ -1318,7 +1482,7 @@ sub BOSEST_updateAutoZone {
sub BOSEST_checkDoubleTap($$) { sub BOSEST_checkDoubleTap($$) {
my ($hash, $channel) = @_; my ($hash, $channel) = @_;
return undef if($channel eq ""); return undef if($channel eq "" or $channel eq "0");
if(!defined($hash->{helper}{dt_nowSelectionUpdatedTS}) or $channel ne $hash->{helper}{dt_nowSelectionUpdatedCH}) { if(!defined($hash->{helper}{dt_nowSelectionUpdatedTS}) or $channel ne $hash->{helper}{dt_nowSelectionUpdatedCH}) {
$hash->{helper}{dt_nowSelectionUpdatedTS} = gettimeofday(); $hash->{helper}{dt_nowSelectionUpdatedTS} = gettimeofday();
@@ -1428,13 +1592,16 @@ sub BOSEST_processXml($$) {
if($hash->{helper}{stateCheck}{enabled}) { if($hash->{helper}{stateCheck}{enabled}) {
#check if state is action state #check if state is action state
if(ReadingsVal($hash->{NAME}, "source", "") eq $hash->{helper}{stateCheck}{actionSource}) { if(ReadingsVal($hash->{NAME}, "contentItemItemName", "") eq $hash->{helper}{stateCheck}{actionContentItemItemName}) {
#call function with $hash as argument $hash->{helper}{stateCheck}{actionActive} = 1;
$hash->{helper}{stateCheck}{function}->($hash); } else {
if($hash->{helper}{stateCheck}{actionActive}) {
#reset if always is not enabled if(ReadingsVal($hash->{NAME}, "contentItemItemName", "") ne $hash->{helper}{stateCheck}{actionContentItemItemName}) {
if(!$hash->{helper}{stateCheck}{always}) { #call function with $hash as argument
$hash->{helper}{stateCheck}{enabled} = 0; $hash->{helper}{stateCheck}{function}->($hash);
$hash->{helper}{stateCheck}{enabled} = 0;
$hash->{helper}{stateCheck}{actionActive} = 0;
}
} }
} }
} }
@@ -2318,7 +2485,7 @@ sub BOSEST_readingsSingleUpdateIfChanged {
<ul> <ul>
BOSEST is used to control a BOSE SoundTouch system (one or more SoundTouch 10, 20 or 30 devices)<br><br> BOSEST is used to control a BOSE SoundTouch system (one or more SoundTouch 10, 20 or 30 devices)<br><br>
<b>Note:</b> The followig libraries are required for this module: <b>Note:</b> The followig libraries are required for this module:
<ul><li>libwww-perl</li> <li>libmojolicious-perl</li> <li>libxml-simple-perl</li> <li>libnet-bonjour-perl</li> <li>libev-perl</li><li>liburi-escape-xs-perl</li><br> <ul><li>libwww-perl</li> <li>libmojolicious-perl</li> <li>libxml-simple-perl</li> <li>libnet-bonjour-perl</li> <li>libev-perl</li><li>liburi-escape-xs-perl</li><li>sox</li><li>libsox-fmt-mp3</li><br>
Use <b>sudo apt-get install libwww-perl libmojolicious-perl libxml-simple-perl libnet-bonjour-perl libev-perl</b> to install this libraries.<br>Please note: Use <b>sudo apt-get install libwww-perl libmojolicious-perl libxml-simple-perl libnet-bonjour-perl libev-perl</b> to install this libraries.<br>Please note:
libmojolicious-perl must be >=5.54, but under wheezy is only 2.x avaible.<br> libmojolicious-perl must be >=5.54, but under wheezy is only 2.x avaible.<br>
Use <b>sudo apt-get install cpanminus</b> and <b>sudo cpanm Mojolicious</b> to update to the newest version<br> Use <b>sudo apt-get install cpanminus</b> and <b>sudo cpanm Mojolicious</b> to update to the newest version<br>
@@ -2389,7 +2556,6 @@ sub BOSEST_readingsSingleUpdateIfChanged {
<li><code><b>speak</b> "message" [0...100] [+x|-x] [en|de|xx]</code> &nbsp;&nbsp;-&nbsp;&nbsp; Text to speak, optional with volume adjustment and language to use. The message to speak may have up to 100 letters</li> <li><code><b>speak</b> "message" [0...100] [+x|-x] [en|de|xx]</code> &nbsp;&nbsp;-&nbsp;&nbsp; Text to speak, optional with volume adjustment and language to use. The message to speak may have up to 100 letters</li>
<li><code><b>speakOff</b> "message" [0...100] [+x|-x] [en|de|xx]</code> &nbsp;&nbsp;-&nbsp;&nbsp; Text to speak, optional with volume adjustment and language to use. The message to speak may have up to 100 letters. Device is switched off after speak</li> <li><code><b>speakOff</b> "message" [0...100] [+x|-x] [en|de|xx]</code> &nbsp;&nbsp;-&nbsp;&nbsp; Text to speak, optional with volume adjustment and language to use. The message to speak may have up to 100 letters. Device is switched off after speak</li>
<li><code><b>ttsVolume</b> [0...100] [+x|-x]</code> &nbsp;&nbsp;-&nbsp;&nbsp; set the TTS volume level in percentage or change volume by ±x from current level</li> <li><code><b>ttsVolume</b> [0...100] [+x|-x]</code> &nbsp;&nbsp;-&nbsp;&nbsp; set the TTS volume level in percentage or change volume by ±x from current level</li>
<li><code><b>ttsDLNAServer</b> "DLNA Server"</code> &nbsp;&nbsp;-&nbsp;&nbsp; set DLNA TTS server, only needed if the DLNA server is not the FHEM server, a DLNA server running on the same server as FHEM is automatically added to the BOSE library</li>
<li><code><b>ttsDirectory</b> "directory"</code> &nbsp;&nbsp;-&nbsp;&nbsp; set DLNA TTS directory. FHEM user needs permissions to write to that directory. </li> <li><code><b>ttsDirectory</b> "directory"</code> &nbsp;&nbsp;-&nbsp;&nbsp; set DLNA TTS directory. FHEM user needs permissions to write to that directory. </li>
<li><code><b>ttsLanguage </b> en|de|xx</code> &nbsp;&nbsp;-&nbsp;&nbsp; set default TTS language (default: en)</li> <li><code><b>ttsLanguage </b> en|de|xx</code> &nbsp;&nbsp;-&nbsp;&nbsp; set default TTS language (default: en)</li>
<li><code><b>ttsSpeakOnError</b> 0|1</code> &nbsp;&nbsp;-&nbsp;&nbsp; 0=disable to speak "not available" text</li> <li><code><b>ttsSpeakOnError</b> 0|1</code> &nbsp;&nbsp;-&nbsp;&nbsp; 0=disable to speak "not available" text</li>