mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 12:49:34 +00:00
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:
parent
f4434200b3
commit
c82e8082f4
@ -1,5 +1,13 @@
|
||||
# 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.
|
||||
- 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: 66_ECMD: new attribute autoReopen
|
||||
- update: 74_AMAD: Version 2.6.8 new feature sendSMS
|
||||
|
@ -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
|
||||
# $Id$
|
||||
#
|
||||
# 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
|
||||
# - BUGFIX: better error handling if no notification was received
|
||||
# - BUGFIX: update system information fixed
|
||||
@ -102,6 +117,7 @@
|
||||
# set tempconf 17 comfort*2 eco*2
|
||||
#
|
||||
# TODOs
|
||||
# - create virtual device (wohnzimmer)
|
||||
# - read/set eco/comfort temperature
|
||||
# - read/set tempOffset
|
||||
# - read/set windowOpen time settings
|
||||
@ -126,6 +142,7 @@ sub EQ3BT_Initialize($) {
|
||||
$hash->{GetFn} = 'EQ3BT_Get';
|
||||
$hash->{SetFn} = 'EQ3BT_Set';
|
||||
$hash->{AttrFn} = 'EQ3BT_Attribute';
|
||||
$hash->{AttrList} = $readingFnAttributes;
|
||||
|
||||
return undef;
|
||||
}
|
||||
@ -138,6 +155,8 @@ sub EQ3BT_Define($$) {
|
||||
my $mac;
|
||||
|
||||
$hash->{STATE} = "initialized";
|
||||
$hash->{VERSION} = "2.0.0";
|
||||
Log3 $hash, 3, "EQ3BT: EQ-3 Bluetooth Thermostat ".$hash->{VERSION};
|
||||
|
||||
if (int(@a) > 3) {
|
||||
return 'EQ3BT: Wrong syntax, must be define <name> EQ3BT <mac address>';
|
||||
@ -146,15 +165,32 @@ sub EQ3BT_Define($$) {
|
||||
$hash->{MAC} = $a[2];
|
||||
}
|
||||
|
||||
EQ3BT_updateHciDevicelist($hash);
|
||||
|
||||
BlockingCall("EQ3BT_pairDevice", $name."|".$hash->{MAC});
|
||||
|
||||
RemoveInternalTimer($hash);
|
||||
InternalTimer(gettimeofday()+60, "EQ3BT_updateStatusWithTimer", $hash, 0);
|
||||
InternalTimer(gettimeofday()+20, "EQ3BT_updateSystemInformationWithTimer", $hash, 0);
|
||||
InternalTimer(gettimeofday()+60, "EQ3BT_updateStatus", $hash, 0);
|
||||
InternalTimer(gettimeofday()+20, "EQ3BT_updateSystemInformation", $hash, 0);
|
||||
|
||||
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 {
|
||||
my ($string) = @_;
|
||||
my ($name, $mac) = split("\\|", $string);
|
||||
@ -184,9 +220,9 @@ sub EQ3BT_Set($@) {
|
||||
#
|
||||
my ($hash, $name, @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,5,0.5,30,1 boost daymode nightmode childlock holidaymode datetime window program";
|
||||
|
||||
my $list = "desiredTemperature:slider,4.5,0.5,29.5,1 updateStatus:noArg boost:on,off mode:manual,automatic eco:noArg comfort:noArg ".
|
||||
"resetErrorCounters:noArg resetConsumption:noArg";
|
||||
|
||||
# check parameters for set function
|
||||
if($workType eq "?") {
|
||||
return SetExtensions($hash, $list, $name, $workType, @params);
|
||||
@ -209,6 +245,10 @@ sub EQ3BT_Set($@) {
|
||||
EQ3BT_setEco($hash);
|
||||
} elsif($workType eq "comfort") {
|
||||
EQ3BT_setComfort($hash);
|
||||
} elsif($workType eq "resetErrorCounters") {
|
||||
EQ3BT_setResetErrorCounters($hash);
|
||||
} elsif($workType eq "resetConsumption") {
|
||||
EQ3BT_setResetConsumption($hash);
|
||||
} elsif($workType eq "childlock") {
|
||||
return "EQ3BT: childlock requires on/off as additional parameter" if(int(@params) < 1);
|
||||
EQ3BT_setChildlock($hash, $params[0]);
|
||||
@ -230,6 +270,26 @@ sub EQ3BT_Set($@) {
|
||||
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 ###
|
||||
sub EQ3BT_updateSystemInformation {
|
||||
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);
|
||||
}
|
||||
|
||||
sub EQ3BT_updateSystemInformationWithTimer {
|
||||
my ($hash) = @_;
|
||||
EQ3BT_updateSystemInformation($hash);
|
||||
InternalTimer(gettimeofday()+7200+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub EQ3BT_updateSystemInformationSuccessful {
|
||||
my ($hash, $handle, $value) = @_;
|
||||
|
||||
InternalTimer(gettimeofday()+7200+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
|
||||
return undef;
|
||||
}
|
||||
|
||||
@ -256,15 +309,13 @@ sub EQ3BT_updateSystemInformationRetry {
|
||||
return undef;
|
||||
}
|
||||
|
||||
### updateStatus ###
|
||||
sub EQ3BT_updateStatusWithTimer {
|
||||
sub EQ3BT_updateSystemInformationFailed {
|
||||
my ($hash) = @_;
|
||||
|
||||
EQ3BT_updateStatus($hash);
|
||||
|
||||
InternalTimer(gettimeofday()+160+int(rand(20)), "EQ3BT_updateStatusWithTimer", $hash, 0);
|
||||
InternalTimer(gettimeofday()+7000+int(rand(180)), "EQ3BT_updateSystemInformation", $hash, 0);
|
||||
return undef;
|
||||
}
|
||||
|
||||
### updateStatus ###
|
||||
sub EQ3BT_updateStatus {
|
||||
my ($hash) = @_;
|
||||
my $name = $hash->{NAME};
|
||||
@ -273,7 +324,7 @@ sub EQ3BT_updateStatus {
|
||||
|
||||
sub EQ3BT_updateStatusSuccessful {
|
||||
my ($hash, $handle, $value) = @_;
|
||||
|
||||
InternalTimer(gettimeofday()+140+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
|
||||
return undef;
|
||||
}
|
||||
|
||||
@ -283,6 +334,12 @@ sub EQ3BT_updateStatusRetry {
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub EQ3BT_updateStatusFailed {
|
||||
my ($hash, $handle, $value) = @_;
|
||||
InternalTimer(gettimeofday()+170+int(rand(60)), "EQ3BT_updateStatus", $hash, 0);
|
||||
return undef;
|
||||
}
|
||||
|
||||
### setDesiredTemperature ###
|
||||
sub EQ3BT_setDesiredTemperature($$) {
|
||||
my ($hash, $desiredTemp) = @_;
|
||||
@ -297,7 +354,7 @@ sub EQ3BT_setDesiredTemperature($$) {
|
||||
sub EQ3BT_setDesiredTemperatureSuccessful {
|
||||
my ($hash, $handle, $tempVal) = @_;
|
||||
my $temp = (hex($tempVal) - 0x4100) / 2;
|
||||
readingsSingleUpdate($hash, "desiredTemperature", $temp, 1);
|
||||
readingsSingleUpdate($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
|
||||
return undef;
|
||||
}
|
||||
|
||||
@ -407,6 +464,7 @@ sub EQ3BT_execGatttool($) {
|
||||
my ($string) = @_;
|
||||
my ($name, $mac, $workType, $handle, $value, $listen) = split("\\|", $string);
|
||||
my $wait = 1;
|
||||
my $hash = $main::defs{$name};
|
||||
|
||||
my $gatttool = qx(which gatttool);
|
||||
chomp $gatttool;
|
||||
@ -415,25 +473,26 @@ sub EQ3BT_execGatttool($) {
|
||||
my $gtResult;
|
||||
|
||||
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*$/) {
|
||||
#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);
|
||||
} else {
|
||||
$wait = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if($value eq "03") {
|
||||
my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
|
||||
my $currentDate = sprintf("%02X%02X%02X%02X%02X", $year+1900-2000, $mon+1, $mday, $hour, $min);
|
||||
$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") {
|
||||
$cmd = "timeout 5 ".$cmd." --listen";
|
||||
$cmd = "timeout 15 ".$cmd." --listen";
|
||||
}
|
||||
|
||||
#redirect stderr to stdout
|
||||
@ -478,6 +537,8 @@ sub EQ3BT_processGatttoolResult($) {
|
||||
my $value = $a[5];
|
||||
my $notification = $a[6];
|
||||
|
||||
delete($hash->{helper}{RUNNING_PID});
|
||||
|
||||
Log3 $hash, 5, "EQ3BT ($name): gatttool return string: $string";
|
||||
|
||||
$hash->{helper}{"handle$workType"} = $handle;
|
||||
@ -489,11 +550,15 @@ sub EQ3BT_processGatttoolResult($) {
|
||||
if(defined($notification)) {
|
||||
EQ3BT_processNotification($hash, $notification);
|
||||
}
|
||||
if($workType =~ /set.*/) {
|
||||
readingsSingleUpdate($hash, "lastChangeBy", "FHEM", 1);
|
||||
}
|
||||
#call WorkTypeSuccessful function
|
||||
my $call = "EQ3BT_".$workType."Successful";
|
||||
#FIXME otherwise temperature is not set after successfull write
|
||||
no strict "refs";
|
||||
&{$call}($hash, $handle, $value);
|
||||
eval {
|
||||
&{$call}($hash, $handle, $value);
|
||||
};
|
||||
use strict "refs";
|
||||
RemoveInternalTimer($hash, "EQ3BT_".$workType."Retry");
|
||||
$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"}++;
|
||||
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);
|
||||
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}{"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 {
|
||||
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 $oldVal = ReadingsVal($hash->{NAME}, "valvePosition", 0);
|
||||
my $consumptionDiff = 0;
|
||||
if($timeSinceLastChange < 300) {
|
||||
if($timeSinceLastChange < 600) {
|
||||
$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);
|
||||
if($consumptionTodaySecSinceLastChange > ($hour*3600+$min*60+$sec)) {
|
||||
readingsSingleUpdate($hash, "consumptionYesterday", $consumptionToday + $consumptionDiff/2, 1);
|
||||
readingsSingleUpdate($hash, "consumptionToday", 0 + $consumptionDiff/2, 1);
|
||||
} 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, "ecoMode", $eco, 1);
|
||||
readingsSingleUpdate($hash, "battery", $batteryStr, 1);
|
||||
readingsSingleUpdate($hash, "boost", $isBoost, 1);
|
||||
readingsSingleUpdate($hash, "consumption", sprintf("%.3f", $consumption+$consumptionDiff), 1);
|
||||
readingsSingleUpdate($hash, "mode", $modeStr, 1);
|
||||
readingsSingleUpdate($hash, "valvePosition", $pct, 1);
|
||||
readingsSingleUpdate($hash, "desiredTemperature", $temp, 1);
|
||||
readingsSingleUpdate($hash, "desiredTemperature", sprintf("%.1f", $temp), 1);
|
||||
}
|
||||
|
||||
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($) {
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
# $Id$
|
||||
#
|
||||
# FHEM module to communicate with BOSE SoundTouch system
|
||||
# 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
|
||||
# - FEATURE: support shuffle/repeat (thx@rockyou)
|
||||
# - BUGFIX: support special characters for TTS (thx@hschuett)
|
||||
@ -201,12 +221,7 @@
|
||||
# - change preset via /key
|
||||
#
|
||||
# TODO
|
||||
# - redesign multiroom functionality (virtual devices: represent the readings of master device
|
||||
# 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)
|
||||
# - set title/album/artist for TTS files (--comment "Title=Title..")
|
||||
# - check if Mojolicious should be used for HTTPGET/HTTPPOST
|
||||
# - 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_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($) {
|
||||
my ($hash) = @_;
|
||||
@ -247,6 +264,7 @@ sub BOSEST_Initialize($) {
|
||||
$hash->{GetFn} = 'BOSEST_Get';
|
||||
$hash->{SetFn} = 'BOSEST_Set';
|
||||
$hash->{AttrFn} = 'BOSEST_Attribute';
|
||||
$hash->{AttrList} = $readingFnAttributes;
|
||||
|
||||
return undef;
|
||||
}
|
||||
@ -281,6 +299,7 @@ sub BOSEST_Define($$) {
|
||||
|
||||
#init statecheck
|
||||
$hash->{helper}{stateCheck}{enabled} = 0;
|
||||
$hash->{helper}{stateCheck}{actionActive} = 0;
|
||||
|
||||
#init switchSource
|
||||
$hash->{helper}{switchSource} = "";
|
||||
@ -288,11 +307,14 @@ sub BOSEST_Define($$) {
|
||||
#init speak channel functionality
|
||||
$hash->{helper}{lastSpokenChannel} = "";
|
||||
|
||||
foreach my $attrname (qw(channel_07 channel_08 channel_09 channel_10 channel_11
|
||||
channel_12 channel_13 channel_14 channel_15 channel_16
|
||||
channel_17 channel_18 channel_19 channel_20 ignoreDeviceIDs
|
||||
ttsDirectory ttsLanguage ttsSpeakOnError ttsDLNAServer ttsVolume
|
||||
speakChannel autoZone)) {
|
||||
my $attrList = "channel_07 channel_08 channel_09 channel_10 channel_11 ".
|
||||
"channel_12 channel_13 channel_14 channel_15 channel_16 ".
|
||||
"channel_17 channel_18 channel_19 channel_20 ignoreDeviceIDs ".
|
||||
"ttsDirectory ttsLanguage ttsSpeakOnError ttsVolume ".
|
||||
"speakChannel autoZone";
|
||||
my @attrListArr = split(" ", $attrList);
|
||||
|
||||
foreach my $attrname (@attrListArr) {
|
||||
addToDevAttrList($name, $attrname);
|
||||
}
|
||||
|
||||
@ -309,7 +331,7 @@ sub BOSEST_Define($$) {
|
||||
$hash->{helper}{supportedBassCmds} = "";
|
||||
|
||||
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
|
||||
InternalTimer(gettimeofday()+30, "BOSEST_startDiscoveryProcess", $hash, 0);
|
||||
|
||||
@ -331,8 +353,6 @@ sub BOSEST_Attribute($$$$) {
|
||||
return "BOSEST: wrong format" if(!defined($value[2]));
|
||||
#update reading for channel_X
|
||||
readingsSingleUpdate($main::defs{$devName}, $attrName, $value[0], 1);
|
||||
} elsif($attrName eq "ttsDLNAServer") {
|
||||
BOSEST_addDLNAServer($main::defs{$devName}, $attrValue);
|
||||
}
|
||||
} elsif($mode eq "del") {
|
||||
if(substr($attrName, 0, 8) eq "channel_") {
|
||||
@ -356,18 +376,18 @@ sub BOSEST_Set($@) {
|
||||
}
|
||||
@params = @params2;
|
||||
|
||||
my $list = "on:noArg off:noArg power:noArg play:noArg
|
||||
mute:on,off,toggle recent source:".$hash->{helper}{supportedSourcesCmds}."
|
||||
shuffle:on,off
|
||||
repeat:all,one,off
|
||||
nextTrack:noArg prevTrack:noArg playTrack speak speakOff
|
||||
playEverywhere:noArg stopPlayEverywhere:noArg createZone addToZone removeFromZone
|
||||
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
|
||||
volume:slider,0,1,100 ".$hash->{helper}{supportedBassCmds}."
|
||||
saveChannel:07,08,09,10,11,12,13,14,15,16,17,18,19,20
|
||||
addDLNAServer:".$hash->{helper}{dlnaServers}."
|
||||
removeDLNAServer:".ReadingsVal($hash->{NAME}, "connectedDLNAServers", "noArg");
|
||||
my $list = "on:noArg off:noArg power:noArg play:noArg ".
|
||||
"playPause:noArg ".
|
||||
"mute:on,off,toggle recent source:".$hash->{helper}{supportedSourcesCmds}.
|
||||
"shuffle:on,off repeat:all,one,off ".
|
||||
"nextTrack:noArg prevTrack:noArg playTrack speak speakOff ".
|
||||
"playEverywhere:noArg stopPlayEverywhere:noArg createZone addToZone removeFromZone ".
|
||||
"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 ".
|
||||
"volume:slider,0,1,100 ".$hash->{helper}{supportedBassCmds}." ".
|
||||
"saveChannel:07,08,09,10,11,12,13,14,15,16,17,18,19,20 ".
|
||||
"addDLNAServer:".$hash->{helper}{dlnaServers}." ".
|
||||
"removeDLNAServer:".ReadingsVal($hash->{NAME}, "connectedDLNAServers", "noArg");
|
||||
|
||||
# check parameters for set function
|
||||
#DEVELOPNEWFUNCTION-1
|
||||
@ -441,6 +461,8 @@ sub BOSEST_Set($@) {
|
||||
BOSEST_stop($hash);
|
||||
} elsif($workType eq "pause") {
|
||||
BOSEST_pause($hash);
|
||||
} elsif($workType eq "playPause") {
|
||||
BOSEST_playPause($hash);
|
||||
} elsif($workType eq "power") {
|
||||
BOSEST_power($hash);
|
||||
} elsif($workType eq "on") {
|
||||
@ -459,7 +481,6 @@ sub BOSEST_Set($@) {
|
||||
} 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" 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 "") {
|
||||
return "BOSEST: Please set ttsDirectory attribute first.
|
||||
FHEM user needs permissions to write to that directory.
|
||||
@ -586,17 +607,18 @@ sub BOSEST_removeDLNAServer($$) {
|
||||
sub BOSEST_saveChannel($$) {
|
||||
my ($hash, $channel) = @_;
|
||||
|
||||
if(ReadingsVal($hash->{NAME}, "state", "stopped") ne "playing") {
|
||||
return "BOSEST: No playing channel. Start a channel and save afterwards.";
|
||||
if(ReadingsVal($hash->{NAME}, "contentItemLocation", "") eq "") {
|
||||
return "BOSEST: No active channel.";
|
||||
}
|
||||
|
||||
#itemname, location, source, sourceaccount
|
||||
#itemname, type, location, source, sourceaccount
|
||||
my $itemName = ReadingsVal($hash->{NAME}, "contentItemItemName", "");
|
||||
my $location = ReadingsVal($hash->{NAME}, "contentItemLocation", "");
|
||||
my $type = ReadingsVal($hash->{NAME}, "contentItemType", "");
|
||||
my $source = ReadingsVal($hash->{NAME}, "contentItemSource", "");
|
||||
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;
|
||||
}
|
||||
|
||||
@ -749,6 +771,7 @@ sub BOSEST_setRecent($$) {
|
||||
|
||||
BOSEST_setContentItem($hash,
|
||||
$hash->{helper}{recents}{$nr}{itemName},
|
||||
$hash->{helper}{recents}{$nr}{type},
|
||||
$hash->{helper}{recents}{$nr}{location},
|
||||
$hash->{helper}{recents}{$nr}{source},
|
||||
$hash->{helper}{recents}{$nr}{sourceAccount});
|
||||
@ -756,8 +779,10 @@ sub BOSEST_setRecent($$) {
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub BOSEST_setContentItem($$$$$) {
|
||||
my ($hash, $itemName, $location, $source, $sourceAccount) = @_;
|
||||
sub BOSEST_setContentItem {
|
||||
my ($hash, $itemName, $type, $location, $source, $sourceAccount) = @_;
|
||||
|
||||
$type = "" if(!defined($type));
|
||||
|
||||
my $postXml = "<ContentItem source=\"".
|
||||
$source.
|
||||
@ -765,6 +790,8 @@ sub BOSEST_setContentItem($$$$$) {
|
||||
$sourceAccount.
|
||||
"\" location=\"".
|
||||
$location.
|
||||
"\" type=\"".
|
||||
$type.
|
||||
"\">".
|
||||
"<itemName>".
|
||||
$itemName.
|
||||
@ -877,10 +904,13 @@ sub BOSEST_setPreset($$) {
|
||||
my $channelVal = AttrVal($hash->{NAME}, sprintf("channel_%02d", $preset), "0");
|
||||
return undef if($channelVal eq "0");
|
||||
my @channel = split("\\|", $channelVal);
|
||||
$channel[1] = "" if(!defined($channel[1]));
|
||||
$channel[2] = "" if(!defined($channel[2]));
|
||||
$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
|
||||
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;
|
||||
}
|
||||
@ -891,6 +921,12 @@ sub BOSEST_play($) {
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub BOSEST_playPause($) {
|
||||
my ($hash) = @_;
|
||||
BOSEST_sendKey($hash, "PLAY_PAUSE");
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub BOSEST_stop($) {
|
||||
my ($hash) = @_;
|
||||
BOSEST_sendKey($hash, "STOP");
|
||||
@ -968,6 +1004,13 @@ sub BOSEST_speak($$$$$) {
|
||||
$lang = AttrVal($hash->{NAME}, "ttsLanguage", "en") if($lang 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
|
||||
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}{source} = ReadingsVal($hash->{NAME}, "source", "");
|
||||
$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}{contentItemType} = ReadingsVal($hash->{NAME}, "contentItemType", "");
|
||||
$hash->{helper}{savedState}{contentItemLocation} = ReadingsVal($hash->{NAME}, "contentItemLocation", "");
|
||||
$hash->{helper}{savedState}{contentItemSource} = ReadingsVal($hash->{NAME}, "contentItemSource", "");
|
||||
$hash->{helper}{savedState}{contentItemSourceAccount} = ReadingsVal($hash->{NAME}, "contentItemSourceAccount", "");
|
||||
@ -995,13 +1040,20 @@ sub BOSEST_restoreSavedState($) {
|
||||
BOSEST_setBass($hash, $hash->{helper}{savedState}{bass});
|
||||
|
||||
#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);
|
||||
} else {
|
||||
BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName},
|
||||
$hash->{helper}{savedState}{contentItemType},
|
||||
$hash->{helper}{savedState}{contentItemLocation},
|
||||
$hash->{helper}{savedState}{contentItemSource},
|
||||
$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;
|
||||
@ -1014,6 +1066,7 @@ sub BOSEST_restoreVolumeAndOff($) {
|
||||
BOSEST_setBass($hash, $hash->{helper}{savedState}{bass});
|
||||
|
||||
BOSEST_setContentItem($hash, $hash->{helper}{savedState}{contentItemItemName},
|
||||
$hash->{helper}{savedState}{contentItemType},
|
||||
$hash->{helper}{savedState}{contentItemLocation},
|
||||
$hash->{helper}{savedState}{contentItemSource},
|
||||
$hash->{helper}{savedState}{contentItemSourceAccount});
|
||||
@ -1029,17 +1082,14 @@ sub BOSEST_downloadGoogleNotAvailable($) {
|
||||
|
||||
my $md5 = md5_hex($lang.$text);
|
||||
my $filename = $ttsDir."/".$md5.".mp3";
|
||||
if (-f $filename) {
|
||||
#file exists already
|
||||
return undef;
|
||||
if (! -f $filename) {
|
||||
BOSEST_retrieveGooglTTSFile($hash, $filename, $md5, $text, $lang);
|
||||
}
|
||||
|
||||
BOSEST_downloadGoogleTTS($hash, $filename, $md5, $text, $lang);
|
||||
|
||||
return undef;
|
||||
}
|
||||
|
||||
sub BOSEST_downloadGoogleTTS($$$$$;$) {
|
||||
sub BOSEST_retrieveGooglTTSFile($$$$$;$) {
|
||||
my ($hash, $filename, $md5, $text, $lang, $callback) = @_;
|
||||
|
||||
my $uri_text = uri_escape($text);
|
||||
@ -1058,22 +1108,67 @@ sub BOSEST_downloadGoogleTTS($$$$$;$) {
|
||||
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($$$$) {
|
||||
my ($hash, $trackname, $volume, $stopAfterSpeak) = @_;
|
||||
|
||||
Log3 $hash, 4, "BOSEST: playMessage $trackname, $volume, $stopAfterSpeak";
|
||||
|
||||
BOSEST_saveCurrentState($hash);
|
||||
|
||||
if($volume ne ReadingsVal($hash->{NAME}, "volume", 0)) {
|
||||
BOSEST_stop($hash);
|
||||
BOSEST_pause($hash);
|
||||
BOSEST_setVolume($hash, $volume);
|
||||
}
|
||||
|
||||
BOSEST_playTrack($hash, $trackname);
|
||||
|
||||
$hash->{helper}{stateCheck}{enabled} = 1;
|
||||
$hash->{helper}{stateCheck}{always} = 0;
|
||||
#after play the speaker sets INVALID_SOURCE
|
||||
$hash->{helper}{stateCheck}{actionSource} = "INVALID_SOURCE";
|
||||
#after play the speaker changes contentItemItemName
|
||||
$hash->{helper}{stateCheck}{actionContentItemItemName} = $trackname;
|
||||
#check if we need to stop after speak
|
||||
if(defined($stopAfterSpeak) && $stopAfterSpeak eq "1") {
|
||||
$hash->{helper}{stateCheck}{function} = \&BOSEST_restoreVolumeAndOff;
|
||||
@ -1110,8 +1205,71 @@ sub BOSEST_deleteOldTTSFiles {
|
||||
$err = setKeyValue("BOSEST_tts_files", join(",", @ttsFiles));
|
||||
}
|
||||
|
||||
sub BOSEST_playGoogleTTS($$$$$$) {
|
||||
sub BOSEST_playGoogleTTS {
|
||||
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);
|
||||
|
||||
@ -1123,13 +1281,12 @@ sub BOSEST_playGoogleTTS($$$$$$) {
|
||||
my $now = time();
|
||||
if($now-$timestamp < 2592000) {
|
||||
#file is not older than 30 days
|
||||
#file exists, call play sub
|
||||
BOSEST_playMessage($hash, $md5, $volume, $stopAfterSpeak);
|
||||
$hash->{helper}{tts}{downloads}{$md5} = 1;
|
||||
return undef;
|
||||
}
|
||||
}
|
||||
|
||||
BOSEST_downloadGoogleTTS($hash, $filename, $md5, $text, $lang, sub {
|
||||
BOSEST_retrieveGooglTTSFile($hash, $filename, $md5, $text, $lang, sub {
|
||||
my ($hash, $filename, $md5, $downloadOk) = @_;
|
||||
|
||||
if($downloadOk) {
|
||||
@ -1141,15 +1298,10 @@ sub BOSEST_playGoogleTTS($$$$$$) {
|
||||
}
|
||||
$err = setKeyValue("BOSEST_tts_files", $val.$md5);
|
||||
$err = setKeyValue($md5, gettimeofday());
|
||||
BOSEST_playMessage($hash, $md5, $volume, $stopAfterSpeak);
|
||||
$hash->{helper}{tts}{downloads}{$md5} = 1;
|
||||
#add silence and play message afterwards
|
||||
} else {
|
||||
if(AttrVal($hash->{NAME}, "ttsSpeakOnError", "1") eq "1") {
|
||||
$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;
|
||||
}
|
||||
$hash->{helper}{tts}{downloads}{$md5} = 10; #download error
|
||||
}
|
||||
});
|
||||
|
||||
@ -1185,23 +1337,35 @@ sub BOSEST_removeMusicServiceAccount($$$) {
|
||||
sub BOSEST_playTrack($$) {
|
||||
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}}) {
|
||||
if($source->{source} eq "STORED_MUSIC" && $source->{status} eq "READY") {
|
||||
#skip servers which don't equal to ttsDLNAServer attribute if set
|
||||
if($ttsDlnaServer ne "") {
|
||||
next if($ttsDlnaServer ne $source->{content});
|
||||
}
|
||||
Log3 $hash, 4, "BOSEST: Search for $trackName on $source->{source}";
|
||||
Log3 $hash, 4, "BOSEST: Search for $trackName on $source->{sourceAccount}";
|
||||
if(my $xmlTrack = BOSEST_searchTrack($hash, $source->{sourceAccount}, $trackName)) {
|
||||
BOSEST_setContentItem($hash,
|
||||
$xmlTrack->{itemName},
|
||||
$xmlTrack->{type},
|
||||
$xmlTrack->{location},
|
||||
$xmlTrack->{source},
|
||||
$xmlTrack->{sourceAccount});
|
||||
$hash->{helper}{ttsdlnaserver} = $source->{sourceAccount};
|
||||
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="'.
|
||||
$dlnaUid.
|
||||
'"><startItem>1</startItem><numItems>100</numItems><searchTerm filter="track">'.
|
||||
'"><startItem>1</startItem><numItems>1</numItems><searchTerm filter="track">'.
|
||||
$trackName.
|
||||
'</searchTerm></search>';
|
||||
|
||||
@ -1318,7 +1482,7 @@ sub BOSEST_updateAutoZone {
|
||||
sub BOSEST_checkDoubleTap($$) {
|
||||
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}) {
|
||||
$hash->{helper}{dt_nowSelectionUpdatedTS} = gettimeofday();
|
||||
@ -1428,13 +1592,16 @@ sub BOSEST_processXml($$) {
|
||||
|
||||
if($hash->{helper}{stateCheck}{enabled}) {
|
||||
#check if state is action state
|
||||
if(ReadingsVal($hash->{NAME}, "source", "") eq $hash->{helper}{stateCheck}{actionSource}) {
|
||||
#call function with $hash as argument
|
||||
$hash->{helper}{stateCheck}{function}->($hash);
|
||||
|
||||
#reset if always is not enabled
|
||||
if(!$hash->{helper}{stateCheck}{always}) {
|
||||
$hash->{helper}{stateCheck}{enabled} = 0;
|
||||
if(ReadingsVal($hash->{NAME}, "contentItemItemName", "") eq $hash->{helper}{stateCheck}{actionContentItemItemName}) {
|
||||
$hash->{helper}{stateCheck}{actionActive} = 1;
|
||||
} else {
|
||||
if($hash->{helper}{stateCheck}{actionActive}) {
|
||||
if(ReadingsVal($hash->{NAME}, "contentItemItemName", "") ne $hash->{helper}{stateCheck}{actionContentItemItemName}) {
|
||||
#call function with $hash as argument
|
||||
$hash->{helper}{stateCheck}{function}->($hash);
|
||||
$hash->{helper}{stateCheck}{enabled} = 0;
|
||||
$hash->{helper}{stateCheck}{actionActive} = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2318,7 +2485,7 @@ sub BOSEST_readingsSingleUpdateIfChanged {
|
||||
<ul>
|
||||
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:
|
||||
<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:
|
||||
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>
|
||||
@ -2389,7 +2556,6 @@ sub BOSEST_readingsSingleUpdateIfChanged {
|
||||
<li><code><b>speak</b> "message" [0...100] [+x|-x] [en|de|xx]</code> - 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> - 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> - set the TTS volume level in percentage or change volume by ±x from current level</li>
|
||||
<li><code><b>ttsDLNAServer</b> "DLNA Server"</code> - 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> - set DLNA TTS directory. FHEM user needs permissions to write to that directory. </li>
|
||||
<li><code><b>ttsLanguage </b> en|de|xx</code> - set default TTS language (default: en)</li>
|
||||
<li><code><b>ttsSpeakOnError</b> 0|1</code> - 0=disable to speak "not available" text</li>
|
||||
|
Loading…
Reference in New Issue
Block a user