mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 18:59:33 +00:00
9c4280a19f
git-svn-id: https://svn.fhem.de/fhem/trunk@24348 2b470e98-0d58-463d-a4d8-8e2adae1ed80
4049 lines
159 KiB
Perl
4049 lines
159 KiB
Perl
# $Id$
|
|
###########################################################################
|
|
#
|
|
# FHEM RHASSPY modul (https://github.com/rhasspy)
|
|
#
|
|
# Originally written 2018 by Tobias Wiedenmann (Thyraz)
|
|
# as FHEM Snips.ai module (thanks to Matthias Kleine)
|
|
#
|
|
# Adapted for RHASSPY 2020/2021 by Beta-User and drhirn
|
|
#
|
|
# Thanks to Beta-User, rudolfkoenig, JensS, cb2sela and all the others
|
|
# who did a great job getting this to work!
|
|
#
|
|
# This file is part of fhem.
|
|
#
|
|
# Fhem is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Fhem is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with fhem. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
###########################################################################
|
|
|
|
package RHASSPY; ##no critic qw(Package)
|
|
use strict;
|
|
use warnings;
|
|
use Carp qw(carp);
|
|
use GPUtils qw(:all);
|
|
use JSON;
|
|
use Encode;
|
|
use HttpUtils;
|
|
#use utf8;
|
|
use List::Util 1.45 qw(max min uniq);
|
|
use Data::Dumper;
|
|
|
|
sub ::RHASSPY_Initialize { goto &Initialize }
|
|
|
|
#Beta-User: no GefFn defined...?
|
|
my %gets = (
|
|
version => q{},
|
|
status => q{}
|
|
);
|
|
|
|
my %sets = (
|
|
speak => [],
|
|
play => [],
|
|
customSlot => [],
|
|
textCommand => [],
|
|
trainRhasspy => [qw(noArg)],
|
|
fetchSiteIds => [qw(noArg)],
|
|
update => [qw(devicemap devicemap_only slots slots_no_training language all)],
|
|
volume => []
|
|
);
|
|
|
|
my $languagevars = {
|
|
'units' => {
|
|
'unitHours' => {
|
|
0 => 'hours',
|
|
1 => 'one hour'
|
|
},
|
|
'unitMinutes' => {
|
|
0 => 'minutes',
|
|
1 => 'one minute'
|
|
},
|
|
'unitSeconds' => {
|
|
0 => 'seconds',
|
|
1 => 'one second'
|
|
}
|
|
},
|
|
'responses' => {
|
|
'DefaultError' => "Sorry but something seems not to work as expected",
|
|
'NoValidData' => "Sorry but the received data is not sufficient to derive any action",
|
|
'NoDeviceFound' => "Sorry but I could not find a matching device",
|
|
'NoMappingFound' => "Sorry but I could not find a suitable mapping",
|
|
'NoNewValDerived' => "Sorry but I could not calculate a new value to set",
|
|
'NoActiveMediaDevice' => "Sorry no active playback device",
|
|
'NoMediaChannelFound' => "Sorry but requested channel seems not to exist",
|
|
'DefaultConfirmation' => "OK",
|
|
'DefaultConfirmationTimeout' => "Sorry too late to confirm",
|
|
'DefaultCancelConfirmation' => "Thanks aborted",
|
|
'SilentCancelConfirmation' => "",
|
|
'DefaultConfirmationReceived' => "ok will do it",
|
|
'DefaultConfirmationNoOutstanding' => "no command is awaiting confirmation",
|
|
'timerSet' => {
|
|
'0' => '$label in room $room has been set to $seconds seconds',
|
|
'1' => '$label in room $room has been set to $minutes minutes $seconds',
|
|
'2' => '$label in room $room has been set to $minutes minutes',
|
|
'3' => '$label in room $room has been set to $hours hours $minutetext',
|
|
'4' => '$label in room $room has been set to $hour o clock $minutes',
|
|
'5' => '$label in room $room has been set to tomorrow $hour o clock $minutes'
|
|
},
|
|
'timerEnd' => {
|
|
'0' => '$label expired',
|
|
'1' => '$label in room $room expired'
|
|
},
|
|
'timerCancellation' => '$label for $room deleted',
|
|
'timeRequest' => 'it is $hour o clock $min minutes',
|
|
'weekdayRequest' => 'today it is $weekDay',
|
|
'duration_not_understood' => "Sorry I could not understand the desired duration",
|
|
'reSpeak_failed' => 'i am sorry i can not remember',
|
|
'Change' => {
|
|
'airHumidity' => 'air humidity in $location is $value percent',
|
|
'battery' => {
|
|
'0' => 'battery level in $location is $value',
|
|
'1' => 'battery level in $location is $value percent'
|
|
},
|
|
'brightness' => '$device was set to $value',
|
|
'setTarget' => '$device is set to $value',
|
|
'soilMoisture' => 'soil moisture in $location is $value percent',
|
|
'temperature' => {
|
|
'0' => 'temperature in $location is $value',
|
|
'1' => 'temperature in $location is $value degrees',
|
|
},
|
|
'desired-temp' => 'target temperature for $location is set to $value degrees',
|
|
'volume' => '$device set to $value',
|
|
'waterLevel' => 'water level in $location is $value percent',
|
|
'knownType' => '$mappingType in $location is $value percent',
|
|
'unknownType' => 'value in $location is $value percent'
|
|
}
|
|
},
|
|
'stateResponses' => {
|
|
'inOperation' => {
|
|
'0' => '$deviceName is ready',
|
|
'1' => '$deviceName is still running'
|
|
},
|
|
'inOut' => {
|
|
'0' => '$deviceName is out',
|
|
'1' => '$deviceName is in'
|
|
},
|
|
'onOff' => {
|
|
'0' => '$deviceName is off',
|
|
'1' => '$deviceName is on'
|
|
},
|
|
'openClose' => {
|
|
'0' => '$deviceName is open',
|
|
'1' => '$deviceName is closed'
|
|
}
|
|
}
|
|
};
|
|
|
|
my $internal_mappings = {
|
|
'Change' => {
|
|
'lightUp' => {
|
|
'Type' => 'brightness',
|
|
'up' => '1'
|
|
},
|
|
'lightDown' => {
|
|
'Type' => 'brightness',
|
|
'up' => '0'
|
|
},
|
|
'tempUp' => {
|
|
'Type' => 'temperature',
|
|
'up' => '1'
|
|
},
|
|
'tempDown' => {
|
|
'Type' => 'temperature',
|
|
'up' => '0'
|
|
},
|
|
'volUp' => {
|
|
'Type' => 'volume',
|
|
'up' => '1'
|
|
},
|
|
'volDown' => {
|
|
'Type' => 'volume',
|
|
'up' => '0'
|
|
},
|
|
'setUp' => {
|
|
'Type' => 'setTarget',
|
|
'up' => '1'
|
|
},
|
|
'setDown' => {
|
|
'Type' => 'setTarget',
|
|
'up' => '0'
|
|
}
|
|
},
|
|
'regex' => {
|
|
'upward' => '(higher|brighter|louder|rise|warmer)',
|
|
'setTarget' => '(brightness|volume|target.volume)'
|
|
},
|
|
'stateResponseType' => {
|
|
'on' => 'onOff',
|
|
'off' => 'onOff',
|
|
'open' => 'openClose',
|
|
'closed' => 'openClose',
|
|
'in' => 'inOut',
|
|
'out' => 'inOut',
|
|
'ready' => 'inOperation',
|
|
'acting' => 'inOperation'
|
|
}
|
|
};
|
|
|
|
my $de_mappings = {
|
|
'on' => 'an',
|
|
'percent' => 'Prozent',
|
|
'stateResponseType' => {
|
|
'an' => 'onOff',
|
|
'aus' => 'onOff',
|
|
'auf' => 'openClose',
|
|
'zu' => 'openClose',
|
|
'eingefahren' => 'inOut',
|
|
'ausgefahren' => 'inOut',
|
|
'läuft' => 'inOperation',
|
|
'fertig' => 'inOperation'
|
|
},
|
|
'ToEn' => {
|
|
'Temperatur' => 'temperature',
|
|
'Luftfeuchtigkeit' => 'airHumidity',
|
|
'Batterie' => 'battery',
|
|
'Wasserstand' => 'waterLevel',
|
|
'Bodenfeuchte' => 'soilMoisture',
|
|
'Helligkeit' => 'brightness',
|
|
'Sollwert' => 'setTarget',
|
|
'Lautstärke' => 'volume',
|
|
'kälter' => 'tempDown',
|
|
'wärmer' => 'tempUp',
|
|
'dunkler' => 'lightDown',
|
|
'heller' => 'lightUp',
|
|
'lauter' => 'volUp',
|
|
'leiser' => 'volDown',
|
|
|
|
},
|
|
'regex' => {
|
|
'upward' => '(höher|heller|lauter|wärmer)',
|
|
'setTarget' => '(Helligkeit|Lautstärke|Sollwert)'
|
|
}
|
|
|
|
};
|
|
|
|
BEGIN {
|
|
|
|
GP_Import(qw(
|
|
addToAttrList
|
|
delFromDevAttrList
|
|
delFromAttrList
|
|
readingsSingleUpdate
|
|
readingsBeginUpdate
|
|
readingsBulkUpdate
|
|
readingsEndUpdate
|
|
readingsDelete
|
|
Log3
|
|
defs
|
|
attr
|
|
cmds
|
|
L
|
|
DAYSECONDS
|
|
HOURSECONDS
|
|
MINUTESECONDS
|
|
init_done
|
|
InternalTimer
|
|
RemoveInternalTimer
|
|
AssignIoPort
|
|
CommandAttr
|
|
CommandDeleteAttr
|
|
IOWrite
|
|
readingFnAttributes
|
|
IsDisabled
|
|
AttrVal
|
|
InternalVal
|
|
ReadingsVal
|
|
ReadingsNum
|
|
devspec2array
|
|
gettimeofday
|
|
toJSON
|
|
setVolume
|
|
AnalyzeCommandChain
|
|
AnalyzeCommand
|
|
CommandDefMod
|
|
CommandDelete
|
|
EvalSpecials
|
|
AnalyzePerlCommand
|
|
perlSyntaxCheck
|
|
parseParams
|
|
ResolveDateWildcards
|
|
HttpUtils_NonblockingGet
|
|
round
|
|
strftime
|
|
makeReadingName
|
|
FileRead
|
|
trim
|
|
looks_like_number
|
|
getAllSets
|
|
))
|
|
|
|
};
|
|
|
|
# MQTT Topics die das Modul automatisch abonniert
|
|
my @topics = qw(
|
|
hermes/intent/+
|
|
hermes/dialogueManager/sessionStarted
|
|
hermes/dialogueManager/sessionEnded
|
|
);
|
|
|
|
sub Initialize {
|
|
my $hash = shift // return;
|
|
|
|
# Consumer
|
|
$hash->{DefFn} = \&Define;
|
|
$hash->{UndefFn} = \&Undefine;
|
|
$hash->{DeleteFn} = \&Delete;
|
|
$hash->{SetFn} = \&Set;
|
|
$hash->{AttrFn} = \&Attr;
|
|
$hash->{AttrList} = "IODev rhasspyIntents:textField-long rhasspyShortcuts:textField-long rhasspyTweaks:textField-long response:textField-long forceNEXT:0,1 disable:0,1 disabledForIntervals languageFile " . $readingFnAttributes;
|
|
$hash->{Match} = q{.*};
|
|
$hash->{ParseFn} = \&Parse;
|
|
$hash->{parseParams} = 1;
|
|
|
|
return;
|
|
}
|
|
|
|
# Device anlegen
|
|
sub Define {
|
|
my $hash = shift;
|
|
my $anon = shift;
|
|
my $h = shift;
|
|
#parseParams: my ( $hash, $a, $h ) = @_;
|
|
|
|
my $name = shift @{$anon};
|
|
my $type = shift @{$anon};
|
|
my $Rhasspy = $h->{baseUrl} // shift @{$anon} // q{http://127.0.0.1:12101};
|
|
my $defaultRoom = $h->{defaultRoom} // shift @{$anon} // q{default};
|
|
$hash->{defaultRoom} = $defaultRoom;
|
|
my $language = $h->{language} // shift @{$anon} // lc AttrVal('global','language','en');
|
|
$hash->{MODULE_VERSION} = '0.4.12';
|
|
$hash->{baseUrl} = $Rhasspy;
|
|
#$hash->{helper}{defaultRoom} = $defaultRoom;
|
|
initialize_Language($hash, $language) if !defined $hash->{LANGUAGE} || $hash->{LANGUAGE} ne $language;
|
|
$hash->{LANGUAGE} = $language;
|
|
$hash->{devspec} = $h->{devspec} // q{room=Rhasspy};
|
|
$hash->{fhemId} = $h->{fhemId} // q{fhem};
|
|
initialize_prefix($hash, $h->{prefix}) if !defined $hash->{prefix} || $hash->{prefix} ne $h->{prefix};
|
|
$hash->{prefix} = $h->{prefix} // q{rhasspy};
|
|
$hash->{encoding} = $h->{encoding} // q{UTF-8};
|
|
$hash->{useGenericAttrs} = $h->{useGenericAttrs} // 1;
|
|
$hash->{'.asyncQueue'} = [];
|
|
#Beta-User: Für's Ändern von defaultRoom oder prefix vielleicht (!?!) hilfreich: https://forum.fhem.de/index.php/topic,119150.msg1135838.html#msg1135838 (Rudi zu resolveAttrRename)
|
|
|
|
if ($hash->{useGenericAttrs}) {
|
|
addToAttrList(q{genericDeviceType});
|
|
#addToAttrList(q{homebridgeMapping});
|
|
}
|
|
|
|
return $init_done ? firstInit($hash) : InternalTimer(time+1, \&firstInit, $hash );
|
|
}
|
|
|
|
sub firstInit {
|
|
my $hash = shift // return;
|
|
|
|
# IO
|
|
AssignIoPort($hash);
|
|
my $IODev = AttrVal($hash->{NAME},'IODev',undef);
|
|
|
|
return if !$init_done || !defined $IODev;
|
|
RemoveInternalTimer($hash);
|
|
|
|
IOWrite($hash, 'subscriptions', join q{ }, @topics) if InternalVal($IODev,'TYPE',undef) eq 'MQTT2_CLIENT';
|
|
|
|
fetchSiteIds($hash) if !ReadingsVal( $hash->{NAME}, 'siteIds', 0 );
|
|
initialize_rhasspyTweaks($hash, AttrVal($hash->{NAME},'rhasspyTweaks', undef ));
|
|
initialize_DialogManager($hash);
|
|
initialize_devicemap($hash);
|
|
|
|
return;
|
|
}
|
|
|
|
sub initialize_Language {
|
|
my $hash = shift // return;
|
|
my $lang = shift // return;
|
|
my $cfg = shift // AttrVal($hash->{NAME},'languageFile',undef);
|
|
|
|
my $cp = $hash->{encoding} // q{UTF-8};
|
|
|
|
#default to english first
|
|
$hash->{helper}->{lng} = $languagevars if !defined $hash->{helper}->{lng} || !$init_done;
|
|
|
|
my ($ret, $content) = _readLanguageFromFile($hash, $cfg);
|
|
return $ret if $ret;
|
|
|
|
my $decoded;
|
|
if ( !eval { $decoded = decode_json(encode($cp,$content)) ; 1 } ) {
|
|
Log3($hash->{NAME}, 1, "JSON decoding error in languagefile $cfg: $@");
|
|
return "languagefile $cfg seems not to contain valid JSON!";
|
|
}
|
|
|
|
my $slots = $decoded->{slots};
|
|
|
|
if ( defined $decoded->{default} ) {
|
|
$decoded = _combineHashes( $decoded->{default}, $decoded->{user} );
|
|
Log3($hash->{NAME}, 4, "try to use user specific sentences and defaults in languagefile $cfg");
|
|
}
|
|
$hash->{helper}->{lng} = _combineHashes( $hash->{helper}->{lng}, $decoded);
|
|
|
|
return if !$init_done;
|
|
for my $key (keys %{$slots}) {
|
|
updateSingleSlot($hash, $key, $slots->{$key});
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub initialize_prefix {
|
|
my $hash = shift // return;
|
|
my $prefix = shift // q{rhasspy};
|
|
my $old_prefix = $hash->{prefix}; #Beta-User: Marker, evtl. müssen wir uns was für Umbenennungen überlegen...
|
|
|
|
return if defined $old_prefix && $prefix eq $old_prefix;
|
|
# provide attributes "rhasspyName" etc. for all devices
|
|
addToAttrList("${prefix}Name");
|
|
addToAttrList("${prefix}Room");
|
|
addToAttrList("${prefix}Mapping:textField-long");
|
|
#addToAttrList("${prefix}Channels:textField-long");
|
|
#addToAttrList("${prefix}Colors:textField-long");
|
|
addToAttrList("${prefix}Group:textField");
|
|
addToAttrList("${prefix}Specials:textField-long");
|
|
|
|
return if !$init_done || !defined $old_prefix;
|
|
my @devs = devspec2array("$hash->{devspec}");
|
|
my @rhasspys = devspec2array("TYPE=RHASSPY:FILTER=prefix=$old_prefix");
|
|
|
|
for my $detail (qw( Name Room Mapping Group Specials)) {
|
|
for my $device (@devs) {
|
|
my $aval = AttrVal($device, "${old_prefix}$detail", undef);
|
|
CommandAttr($hash, "$device ${prefix}$detail $aval") if $aval;
|
|
CommandDeleteAttr($hash, "$device ${old_prefix}$detail") if @rhasspys < 2;
|
|
}
|
|
delFromAttrList("${old_prefix}$detail") if @rhasspys < 2;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
# Device löschen
|
|
sub Undefine {
|
|
my $hash = shift // return;
|
|
|
|
RemoveInternalTimer($hash);
|
|
|
|
return;
|
|
}
|
|
|
|
sub Delete {
|
|
my $hash = shift // return;
|
|
my $prefix = $hash->{prefix} // return;
|
|
RemoveInternalTimer($hash);
|
|
|
|
# DELETE POD AFTER TESTS ARE COMPLETED
|
|
=begin comment
|
|
|
|
#Beta-User: globale Attribute löschen
|
|
for (devspec2array("${prefix}Mapping=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Mapping:textField-long");
|
|
}
|
|
for (devspec2array("${prefix}Name=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Name");
|
|
}
|
|
for (devspec2array("${prefix}Room=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Room");
|
|
}
|
|
for (devspec2array("${prefix}Channels=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Channels");
|
|
}
|
|
for (devspec2array("${prefix}Colors=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Colors");
|
|
}
|
|
for (devspec2array("${prefix}Specials=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Specials");
|
|
}
|
|
for (devspec2array("${prefix}Group=.+")) {
|
|
delFromDevAttrList($_,"${prefix}Group");
|
|
}
|
|
=end comment
|
|
=cut
|
|
return;
|
|
}
|
|
|
|
# Set Befehl aufgerufen
|
|
sub Set {
|
|
my $hash = shift;
|
|
my $anon = shift;
|
|
my $h = shift;
|
|
#parseParams: my ( $hash, $a, $h ) = @_;
|
|
my $name = shift @{$anon};
|
|
my $command = shift @{$anon} // q{};
|
|
my @values = @{$anon};
|
|
return "Unknown argument $command, choose one of "
|
|
. join(q{ }, map {
|
|
@{$sets{$_}} ? $_
|
|
.q{:}
|
|
.join q{,}, @{$sets{$_}} : $_} sort keys %sets)
|
|
|
|
if !defined $sets{$command};
|
|
|
|
Log3($name, 5, "set $command - value: " . join q{ }, @values);
|
|
|
|
my $dispatch = {
|
|
updateSlots => \&updateSlots,
|
|
trainRhasspy => \&trainRhasspy,
|
|
fetchSiteIds => \&fetchSiteIds
|
|
};
|
|
|
|
return $dispatch->{$command}->($hash) if ref $dispatch->{$command} eq 'CODE';
|
|
|
|
$values[0] = $h->{text} if ( $command eq 'speak' || $command eq 'textCommand' ) && defined $h->{text};
|
|
|
|
if ( $command eq 'play' || $command eq 'volume' ) {
|
|
$values[0] = $h->{siteId} if defined $h->{siteId};
|
|
$values[1] = $h->{path} if defined $h->{path};
|
|
$values[1] = $h->{volume} if defined $h->{volume};
|
|
}
|
|
|
|
$dispatch = {
|
|
speak => \&sendSpeakCommand,
|
|
textCommand => \&sendTextCommand,
|
|
play => \&setPlayWav,
|
|
volume => \&setVolume
|
|
};
|
|
|
|
return Log3($name, 3, "set $name $command requires at least one argument!") if !@values;
|
|
|
|
my $params = join q{ }, @values; #error case: playWav => PERL WARNING: Use of uninitialized value within @values in join or string
|
|
$params = $h if defined $h->{text} || defined $h->{path} || defined $h->{volume};
|
|
return $dispatch->{$command}->($hash, $params) if ref $dispatch->{$command} eq 'CODE';
|
|
|
|
if ($command eq 'update') {
|
|
if ($values[0] eq 'language') {
|
|
return initialize_Language($hash, $hash->{LANGUAGE});
|
|
}
|
|
if ($values[0] eq 'devicemap') {
|
|
initialize_devicemap($hash);
|
|
$hash->{'.needTraining'} = 1;
|
|
return updateSlots($hash);
|
|
}
|
|
if ($values[0] eq 'devicemap_only') {
|
|
return initialize_devicemap($hash);
|
|
}
|
|
if ($values[0] eq 'slots') {
|
|
$hash->{'.needTraining'} = 1;
|
|
return updateSlots($hash);
|
|
}
|
|
if ($values[0] eq 'slots_no_training') {
|
|
initialize_devicemap($hash);
|
|
return updateSlots($hash);
|
|
}
|
|
if ($values[0] eq 'all') {
|
|
initialize_Language($hash, $hash->{LANGUAGE});
|
|
initialize_devicemap($hash);
|
|
$hash->{'.needTraining'} = 1;
|
|
return updateSlots($hash);
|
|
}
|
|
}
|
|
|
|
if ($command eq 'customSlot') {
|
|
my $slotname = $h->{slotname} // shift @values;
|
|
my $slotdata = $h->{slotdata} // shift @values;
|
|
my $overwr = $h->{overwrite} // shift @values;
|
|
my $training = $h->{training} // shift @values;
|
|
return updateSingleSlot($hash, $slotname, $slotdata, $overwr, $training);
|
|
}
|
|
return;
|
|
}
|
|
|
|
# Attribute setzen / löschen
|
|
sub Attr {
|
|
my $command = shift;
|
|
my $name = shift;
|
|
my $attribute = shift // return;
|
|
my $value = shift;
|
|
my $hash = $defs{$name} // return;
|
|
|
|
# IODev Attribut gesetzt
|
|
if ($attribute eq 'IODev') {
|
|
return;
|
|
}
|
|
|
|
if ( $attribute eq 'rhasspyShortcuts' ) {
|
|
for ( keys %{ $hash->{helper}{shortcuts} } ) {
|
|
delete $hash->{helper}{shortcuts}{$_};
|
|
}
|
|
if ($command eq 'set') {
|
|
return init_shortcuts($hash, $value);
|
|
}
|
|
}
|
|
|
|
if ( $attribute eq 'rhasspyIntents' ) {
|
|
for ( keys %{ $hash->{helper}{custom} } ) {
|
|
delete $hash->{helper}{custom}{$_};
|
|
}
|
|
if ($command eq 'set') {
|
|
return init_custom_intents($hash, $value);
|
|
}
|
|
}
|
|
|
|
if ( $attribute eq 'rhasspyTweaks' ) {
|
|
for ( keys %{ $hash->{helper}{tweaks} } ) {
|
|
delete $hash->{helper}{tweaks}{$_};
|
|
}
|
|
if ($command eq 'set') {
|
|
return initialize_rhasspyTweaks($hash, $value);
|
|
}
|
|
}
|
|
|
|
if ( $attribute eq 'languageFile' ) {
|
|
if ($command ne 'set') {
|
|
delete $hash->{CONFIGFILE};
|
|
delete $attr{$name}{languageFile};
|
|
delete $hash->{helper}{lng};
|
|
$value = undef;
|
|
}
|
|
return initialize_Language($hash, $hash->{LANGUAGE}, $value);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub init_shortcuts {
|
|
my $hash = shift // return;
|
|
my $attrVal = shift // return;
|
|
|
|
my ($intent, $perlcommand, $device, $err );
|
|
for my $line (split m{\n}x, $attrVal) {
|
|
#old syntax
|
|
if ($line !~ m{\A[\s]*i=}x) {
|
|
($intent, $perlcommand) = split m{=}x, $line, 2;
|
|
$err = perlSyntaxCheck( $perlcommand );
|
|
return "$err in $line" if $err && $init_done;
|
|
$hash->{helper}{shortcuts}{$intent}{perl} = $perlcommand;
|
|
$hash->{helper}{shortcuts}{$intent}{NAME} = $hash->{NAME};
|
|
next;
|
|
}
|
|
next if !length $line;
|
|
my($unnamed, $named) = parseParams($line);
|
|
#return "unnamed parameters are not supported! (line: $line)" if ($unnamed) > 1 && $init_done;
|
|
$intent = $named->{i};
|
|
if (defined($named->{f})) {
|
|
$hash->{helper}{shortcuts}{$intent}{fhem} = $named->{f};
|
|
} elsif (defined($named->{p})) {
|
|
$err = perlSyntaxCheck( $perlcommand );
|
|
return "$err in $line" if $err && $init_done;
|
|
$hash->{helper}{shortcuts}{$intent}{perl} = $named->{p};
|
|
} elsif ($init_done && !defined $named->{r}) {
|
|
return "Either a fhem or perl command or a response have to be provided!";
|
|
}
|
|
$hash->{helper}{shortcuts}{$intent}{NAME} = $named->{d} if defined $named->{d};
|
|
$hash->{helper}{shortcuts}{$intent}{response} = $named->{r} if defined $named->{r};
|
|
if ( defined $named->{c} ) {
|
|
$hash->{helper}{shortcuts}{$intent}{conf_req} = !looks_like_number($named->{c}) ? $named->{c} : 'default';
|
|
if (defined $named->{ct}) {
|
|
$hash->{helper}{shortcuts}{$intent}{conf_timeout} = looks_like_number($named->{ct}) ? $named->{ct} : 15;
|
|
} else {
|
|
$hash->{helper}{shortcuts}{$intent}{conf_timeout} = looks_like_number($named->{c}) ? $named->{c} : 15;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub initialize_rhasspyTweaks {
|
|
my $hash = shift // return;
|
|
my $attrVal = shift // return;
|
|
|
|
my ($tweak, $values, $device, $err );
|
|
for my $line (split m{\n}x, $attrVal) {
|
|
next if !length $line;
|
|
if ($line =~ m{\A[\s]*timerLimits[\s]*=}x) {
|
|
($tweak, $values) = split m{=}x, $line, 2;
|
|
$tweak = trim($tweak);
|
|
return "Error in $line! Provide 5 comma separated numeric values!" if !length $values && $init_done;
|
|
my @test = split m{,}x, $values;
|
|
return "Error in $line! Provide 5 comma separated numeric values!" if @test != 5 && $init_done;
|
|
#$values = qq{($values)} if $values !~ m{\A([^()]*)\z}x;
|
|
$hash->{helper}{tweaks}{$tweak} = [@test];
|
|
next;
|
|
}
|
|
|
|
if ($line =~ m{\A[\s]*timerSounds[\s]*=}x) {
|
|
($tweak, $values) = split m{=}x, $line, 2;
|
|
$tweak = trim($tweak);
|
|
return "Error in $line! No content provided!" if !length $values && $init_done;
|
|
my($unnamedParams, $namedParams) = parseParams($values);
|
|
return "Error in $line! Provide at least one key-value pair!" if ( @{$unnamedParams} || !keys %{$namedParams} ) && $init_done;
|
|
$hash->{helper}{tweaks}{$tweak} = $namedParams;
|
|
next;
|
|
}
|
|
|
|
if ($line =~ m{\A[\s]*useGenericAttrs[\s]*=}x) {
|
|
($tweak, $values) = split m{=}x, $line, 2;
|
|
$tweak = trim($tweak);
|
|
return "Error in $line! No content provided!" if !length $values && $init_done;
|
|
my($unnamedParams, $namedParams) = parseParams($values);
|
|
return "Error in $line! Provide at least one key-value pair!" if ( @{$unnamedParams} || !keys %{$namedParams} ) && $init_done;
|
|
$hash->{helper}{tweaks}{$tweak} = $namedParams;
|
|
next;
|
|
}
|
|
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub initialize_DialogManager {
|
|
my $hash = shift // return;
|
|
my $language = $hash->{LANGUAGE};
|
|
my $fhemId = $hash->{fhemId};
|
|
|
|
=pod disable some intents by default https://rhasspy.readthedocs.io/en/latest/reference/#dialogue-manager
|
|
hermes/dialogueManager/configure (JSON)
|
|
|
|
Sets the default intent filter for all subsequent dialogue sessions
|
|
intents: [object] - Intents to enable/disable (empty for all intents)
|
|
intentId: string - Name of intent
|
|
enable: bool - true if intent should be eligible for recognition
|
|
siteId: string = "default" - Hermes site ID
|
|
=cut
|
|
my $sendData = {
|
|
siteId => $fhemId,
|
|
intents => [{intentId => "${language}.${fhemId}.ConfirmAction", enable => "false"}]
|
|
};
|
|
|
|
my $json = toJSON($sendData);
|
|
|
|
IOWrite($hash, 'publish', qq{hermes/dialogueManager/configure $json});
|
|
return;
|
|
}
|
|
|
|
sub init_custom_intents {
|
|
my $hash = shift // return;
|
|
my $attrVal = shift // return;
|
|
|
|
for my $line (split m{\n}x, $attrVal) {
|
|
next if !length $line;
|
|
#return "invalid line $line" if $line !~ m{(?<intent>[^=]+)\s*=\s*(?<perlcommand>(?<function>([^(]+))\((?<arg>.*)(\))\s*)}x;
|
|
return "invalid line $line" if $line !~ m{
|
|
(?<intent>[^=]+)\s* #string up to =, w/o ending whitespace
|
|
=\s* #separator = and potential whitespace
|
|
(?<perlcommand> #identifier
|
|
(?<function>([^(]+))#string up to opening bracket
|
|
\( #opening bracket
|
|
(?<arg>.*)(\))\s*) #everything up to the closing bracket, w/o ending whitespace
|
|
}xms;
|
|
my $intent = trim($+{intent});
|
|
return "no intent found in $line!" if (!$intent || $intent eq q{}) && $init_done;
|
|
my $function = trim($+{function});
|
|
return "invalid function in line $line" if $function =~ m{\s+}x;
|
|
my $perlcommand = trim($+{perlcommand});
|
|
my $err = perlSyntaxCheck( $perlcommand );
|
|
return "$err in $line" if $err && $init_done;
|
|
|
|
#$hash->{helper}{custom}{$+{intent}}{perl} = $perlcommand; #Beta-User: delete after testing!
|
|
$hash->{helper}{custom}{$intent}{function} = $function;
|
|
|
|
my $args = trim($+{arg});
|
|
my @params;
|
|
for my $ar (split m{,}x, $args) {
|
|
$ar =trim($ar);
|
|
#next if $ar eq q{}; #Beta-User having empty args might be intented...
|
|
push @params, $ar;
|
|
}
|
|
|
|
$hash->{helper}{custom}{$+{intent}}{args} = \@params;
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
sub initialize_devicemap {
|
|
my $hash = shift // return;
|
|
|
|
my $devspec = $hash->{devspec};
|
|
delete $hash->{helper}{devicemap};
|
|
|
|
my @devices = devspec2array($devspec);
|
|
|
|
# when called with just one keyword, devspec2array may return the keyword, even if the device doesn't exist...
|
|
return if !@devices;
|
|
|
|
for (@devices) {
|
|
_analyze_genDevType($hash, $_) if $hash->{useGenericAttrs};
|
|
_analyze_rhassypAttr($hash, $_);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
sub _analyze_rhassypAttr {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
|
|
my $prefix = $hash->{prefix};
|
|
|
|
return if !defined AttrVal($device,"${prefix}Room",undef)
|
|
&& !defined AttrVal($device,"${prefix}Name",undef)
|
|
&& !defined AttrVal($device,"${prefix}Channels",undef)
|
|
&& !defined AttrVal($device,"${prefix}Colors",undef)
|
|
&& !defined AttrVal($device,"${prefix}Group",undef)
|
|
&& !defined AttrVal($device,"${prefix}Specials",undef);
|
|
|
|
#rhasspyRooms ermitteln
|
|
my @rooms;
|
|
my $attrv = AttrVal($device,"${prefix}Room",undef);
|
|
@rooms = split m{,}x, lc $attrv if defined $attrv;
|
|
@rooms = split m{,}xi, $hash->{helper}{devicemap}{devices}{$device}->{rooms} if !@rooms && defined $hash->{helper}{devicemap}{devices}{$device}->{rooms};
|
|
if (!@rooms) {
|
|
$rooms[0] = $hash->{defaultRoom};
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}->{rooms} = join q{,}, @rooms;
|
|
|
|
#rhasspyNames ermitteln
|
|
my @names;
|
|
$attrv = AttrVal($device,"${prefix}Name",AttrVal($device,'alias',$device));
|
|
push @names, split m{,}x, lc $attrv;
|
|
$hash->{helper}{devicemap}{devices}{$device}->{alias} = $names[0];
|
|
|
|
for my $dn (@names) {
|
|
for (@rooms) {
|
|
$hash->{helper}{devicemap}{rhasspyRooms}{$_}{$dn} = $device;
|
|
}
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}->{names} = join q{,}, @names;
|
|
|
|
for my $item ('Channels', 'Colors') {
|
|
my @rows = split m{\n}x, AttrVal($device, "${prefix}${item}", q{});
|
|
|
|
for my $row (@rows) {
|
|
my ($key, $val) = split m{=}x, $row, 2;
|
|
next if !$val;
|
|
for my $rooms (@rooms) {
|
|
push @{$hash->{helper}{devicemap}{$item}{$rooms}{$key}}, $device if !grep { m{\A$device\z}x } @{$hash->{helper}{devicemap}{$item}{$rooms}{$key}};
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}{$item}{$key} = $val;
|
|
}
|
|
}
|
|
|
|
#Specials
|
|
my @lines = split m{\n}x, AttrVal($device, "${prefix}Specials", q{});
|
|
for my $line (@lines) {
|
|
my ($key, $val) = split m{:}x, $line, 2;
|
|
next if !$val;
|
|
|
|
if ($key eq 'group') {
|
|
my($unnamed, $named) = parseParams($val);
|
|
my $specials = {};
|
|
my $partOf = $named->{partOf} // shift @{$unnamed};
|
|
$specials->{partOf} = $partOf if defined $partOf;
|
|
$specials->{async_delay} = $named->{async_delay} if defined $named->{async_delay};
|
|
$specials->{prio} = $named->{prio} if defined $named->{prio};
|
|
|
|
$hash->{helper}{devicemap}{devices}{$device}{group_specials} = $specials;
|
|
}
|
|
if ($key eq 'colorForceHue2rgb') {
|
|
$hash->{helper}{devicemap}{devices}{$device}{color_specials}{forceHue2rgb} = $val;
|
|
}
|
|
if ($key eq 'colorCommandMap') {
|
|
my($unnamed, $named) = parseParams($val);
|
|
$hash->{helper}{devicemap}{devices}{$device}{color_specials}{CommandMap} = $named if defined$named;
|
|
}
|
|
if ($key eq 'colorTempMap') {
|
|
my($unnamed, $named) = parseParams($val);
|
|
$hash->{helper}{devicemap}{devices}{$device}{color_specials}{Colortemp} = $named if defined$named;
|
|
}
|
|
if ($key eq 'venetianBlind') {
|
|
my($unnamed, $named) = parseParams($val);
|
|
my $specials = {};
|
|
my $vencmd = $named->{setter} // shift @{$unnamed};
|
|
my $vendev = $named->{device} // shift @{$unnamed};
|
|
$specials->{setter} = $vencmd if defined $vencmd;
|
|
$specials->{device} = $vendev if defined $vendev;
|
|
$specials->{CustomCommand} = $named->{CustomCommand} if defined $named->{CustomCommand};
|
|
|
|
$hash->{helper}{devicemap}{devices}{$device}{venetian_specials} = $specials if defined $vencmd || defined $vendev;
|
|
}
|
|
}
|
|
|
|
#Hash mit {FHEM-Device-Name}{$intent}{$type}?
|
|
my $mappingsString = AttrVal($device, "${prefix}Mapping", q{});
|
|
for (split m{\n}x, $mappingsString) {
|
|
my ($key, $val) = split m{:}x, $_, 2;
|
|
#$key = lc($key);
|
|
#$val = lc($val);
|
|
my %currentMapping = splitMappingString($val);
|
|
next if !%currentMapping;
|
|
# Übersetzen, falls möglich:
|
|
$currentMapping{type} =
|
|
defined $currentMapping{type} ?
|
|
$de_mappings->{ToEn}->{$currentMapping{type}} // $currentMapping{type} // $key
|
|
: $key;
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents}{$key}->{$currentMapping{type}} = \%currentMapping;
|
|
}
|
|
|
|
my @groups;
|
|
$attrv = AttrVal($device,"${prefix}Group", undef);
|
|
$attrv = $attrv // AttrVal($device,'group', undef);
|
|
$hash->{helper}{devicemap}{devices}{$device}{groups} = lc $attrv if $attrv;
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
sub _analyze_genDevType {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
|
|
my $prefix = $hash->{prefix};
|
|
|
|
#prerequesite: gdt has to be set!
|
|
my $gdt = AttrVal($device, 'genericDeviceType', undef) // return;
|
|
|
|
my @names;
|
|
my $attrv;
|
|
#additional names?
|
|
if (!defined AttrVal($device,"${prefix}Name", undef)) {
|
|
|
|
$attrv = AttrVal($device,'alexaName', undef);
|
|
push @names, split m{;}x, lc $attrv if $attrv;
|
|
|
|
$attrv = AttrVal($device,'siriName',undef);
|
|
push @names, split m{,}x, lc $attrv if $attrv;
|
|
|
|
my $alias = lc AttrVal($device,'alias',$device);
|
|
$names[0] = $alias if !@names;
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}->{alias} = $names[0] if $names[0];
|
|
|
|
@names = get_unique(\@names);
|
|
$hash->{helper}{devicemap}{devices}{$device}->{names} = join q{,}, @names if $names[0];
|
|
|
|
my @rooms;
|
|
if (!defined AttrVal($device,"${prefix}Room", undef)) {
|
|
$attrv = AttrVal($device,'alexaRoom', undef);
|
|
push @rooms, split m{,}x, lc $attrv if $attrv;
|
|
|
|
$attrv = AttrVal($device,'room',undef);
|
|
push @rooms, split m{,}x, lc $attrv if $attrv;
|
|
$rooms[0] = $hash->{defaultRoom} if !@rooms;
|
|
}
|
|
|
|
@rooms = get_unique(\@rooms);
|
|
|
|
for my $dn (@names) {
|
|
for (@rooms) {
|
|
$hash->{helper}{devicemap}{rhasspyRooms}{$_}{$dn} = $device;
|
|
}
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}->{rooms} = join q{,}, @rooms;
|
|
|
|
$attrv = AttrVal($device,'group', undef);
|
|
$hash->{helper}{devicemap}{devices}{$device}{groups} = lc $attrv if $attrv;
|
|
|
|
my $hbmap = AttrVal($device, 'homeBridgeMapping', q{});
|
|
my $allset = getAllSets($device);
|
|
my $currentMapping;
|
|
|
|
if ( ($gdt eq 'switch' || $gdt eq 'light') && $allset =~ m{\bo[nf]+([\b:\s]|\Z)}xms ) {
|
|
$currentMapping =
|
|
{ GetOnOff => { GetOnOff => {currentVal => 'state', type => 'GetOnOff', valueOff => 'off'}},
|
|
SetOnOff => { SetOnOff => {cmdOff => 'off', type => 'SetOnOff', cmdOn => 'on'}}
|
|
};
|
|
if ( $gdt eq 'light' && $allset =~ m{\bdim([\b:\s]|\Z)}xms ) {
|
|
my $maxval = InternalVal($device, 'TYPE', 'unknown') eq 'ZWave' ? 99 : 100;
|
|
$currentMapping->{SetNumeric} = {
|
|
brightness => { cmd => 'dim', currentVal => 'state', maxVal => $maxval, minVal => '0', step => '3', type => 'brightness'}};
|
|
}
|
|
|
|
elsif ( $gdt eq 'light' && $allset =~ m{\bpct([\b:\s]|\Z)}xms ) {
|
|
$currentMapping->{SetNumeric} = {
|
|
brightness => { cmd => 'pct', currentVal => 'pct', maxVal => '100', minVal => '0', step => '5', type => 'brightness'}};
|
|
}
|
|
|
|
elsif ( $gdt eq 'light' && $allset =~ m{\bbrightness([\b:\s]|\Z)}xms ) {
|
|
$currentMapping->{SetNumeric} = {
|
|
brightness => { cmd => 'brightness', currentVal => 'brightness', maxVal => '255', minVal => '0', step => '10', map => 'percent', type => 'brightness'}};
|
|
}
|
|
$currentMapping = _analyze_genDevType_setter( $allset, $currentMapping );
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents} = $currentMapping;
|
|
}
|
|
elsif ( $gdt eq 'thermostat' ) {
|
|
my $desTemp = $allset =~ m{\b(desiredTemp)([\b:\s]|\Z)}xms ? $1 : 'desired-temp';
|
|
my $measTemp = InternalVal($device, 'TYPE', 'unknown') eq 'CUL_HM' ? 'measured-temp' : 'temperature';
|
|
$currentMapping =
|
|
{ GetNumeric => { 'desired-temp' => {currentVal => $desTemp, type => 'desired-temp'},
|
|
temperature => {currentVal => $measTemp, type => 'temperature'}},
|
|
SetNumeric => {'desired-temp' => { cmd => $desTemp, currentVal => $desTemp, maxVal => '28', minVal => '10', step => '0.5', type => 'temperature'}}
|
|
};
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents} = $currentMapping;
|
|
}
|
|
|
|
elsif ( $gdt eq 'thermometer' ) {
|
|
my $r = $defs{$device}{READINGS};
|
|
if($r) {
|
|
for (sort keys %{$r}) {
|
|
if ( $_ =~ m{\A(?<id>temperature|humidity)\z}x ) {
|
|
$currentMapping->{GetNumeric}->{$+{id}} = {currentVal => $+{id}, type => $+{id} };
|
|
}
|
|
}
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents} = $currentMapping;
|
|
}
|
|
|
|
elsif ( $gdt eq 'blind' ) {
|
|
if ( $allset =~ m{\bdim([\b:\s]|\Z)}xms ) {
|
|
my $maxval = InternalVal($device, 'TYPE', 'unknown') eq 'ZWave' ? 99 : 100;
|
|
$currentMapping =
|
|
{ GetNumeric => { dim => {currentVal => 'state', type => 'setTarget' } },
|
|
GetOnOff => { GetOnOff => { currentVal=>'dim', valueOn=>$maxval } },
|
|
SetOnOff => { SetOnOff => {cmdOff => 'dim 0', type => 'SetOnOff', cmdOn => "dim $maxval"} },
|
|
SetNumeric => { setTarget => { cmd => 'dim', currentVal => 'state', maxVal => $maxval, minVal => '0', step => '11', type => 'setTarget'} }
|
|
};
|
|
}
|
|
|
|
elsif ( $allset =~ m{\bpct([\b:\s]|\Z)}xms ) {
|
|
$currentMapping = {
|
|
GetNumeric => { 'pct' => {currentVal => 'pct', type => 'setTarget'} },
|
|
GetOnOff => { GetOnOff => {currentVal=>'pct', valueOn=>'100' } },
|
|
SetOnOff => { SetOnOff => {cmdOff => 'pct 0', type => 'SetOnOff', cmdOn => 'pct 100'} },
|
|
SetNumeric => { setTarget => { cmd => 'pct', currentVal => 'pct', maxVal => '100', minVal => '0', step => '13', type => 'setTarget'} }
|
|
};
|
|
}
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents} = $currentMapping;
|
|
}
|
|
|
|
if ( $gdt eq 'media' ) { #genericDeviceType media
|
|
$currentMapping = {
|
|
GetOnOff => { GetOnOff => {currentVal => 'state', type => 'GetOnOff', valueOff => 'off'}},
|
|
SetOnOff => { SetOnOff => {cmdOff => 'off', type => 'SetOnOff', cmdOn => 'on'} },
|
|
GetNumeric => { 'volume' => {currentVal => 'volume', type => 'volume' } }
|
|
};
|
|
|
|
$currentMapping = _analyze_genDevType_setter( $allset, $currentMapping );
|
|
$hash->{helper}{devicemap}{devices}{$device}{intents} = $currentMapping;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
sub _analyze_genDevType_setter {
|
|
my $setter = shift;
|
|
my $mapping = shift // {};
|
|
|
|
my $allValMappings = {
|
|
MediaControls => {
|
|
cmdPlay => 'play', cmdPause => 'pause' ,cmdStop => 'stop', cmdBack => 'previous', cmdFwd => 'next', chanUp => 'channelUp', chanDown => 'channelDown' }
|
|
};
|
|
for my $okey ( keys %{$allValMappings} ) {
|
|
my $ikey = $allValMappings->{$okey};
|
|
for ( keys %{$ikey} ) {
|
|
my $val = $ikey->{$_};
|
|
$mapping->{$okey}->{$okey}->{$_} = $val if $setter =~ m{\b$val([\b:\s]|\Z)}xms;
|
|
}
|
|
}
|
|
my $allKeyMappings = {
|
|
SetNumeric => {
|
|
volume => { cmd => 'volume', currentVal => 'volume', maxVal => '100', minVal => '0', step => '2', type => 'volume'},
|
|
channel => { cmd => 'channel', currentVal => 'channel', step => '1', type => 'channel'}
|
|
},
|
|
SetColorParms => { hue => { cmd => 'hue', currentVal => 'hue', type => 'hue', map => 'percent'},
|
|
color => { cmd => 'color', currentVal => 'color', type => 'color', map => 'percent'},
|
|
sat => { cmd => 'sat', currentVal => 'sat', type => 'sat', map => 'percent'},
|
|
ct => { cmd => 'ct', currentVal => 'ct', type => 'ct', map => 'percent'},
|
|
rgb => { cmd => 'rgb', currentVal => 'rgb', type => 'rgb'},
|
|
color_temp => { cmd => 'color_temp', currentVal => 'color_temp', type => 'ct', map => 'percent'},
|
|
RGB => { cmd => 'RGB', currentVal => 'RGB', type => 'rgb'},
|
|
hex => { cmd => 'hex', currentVal => 'hex', type => 'rgb'},
|
|
saturation => { cmd => 'saturation', currentVal => 'saturation', type => 'sat', map => 'percent'}
|
|
}
|
|
};
|
|
for my $okey ( keys %{$allKeyMappings} ) {
|
|
my $ikey = $allKeyMappings->{$okey};
|
|
for ( keys %{$ikey} ) {
|
|
$mapping->{$okey}->{$ikey->{$_}->{type}} = $ikey->{$_} if $setter =~ m{\b$_([\b:\s]|\Z)}xms;
|
|
#for my $col (qw(ct hue color sat)) {
|
|
if ( $okey eq 'SetColorParms') { #=~ m{\A(ct|hue|color|sat)\z}xms ) {
|
|
my $col = $_;
|
|
if ($setter =~ m{\b${col}:[^\s\d]+,(?<min>[0-9.]+),(?<step>[0-9.]+),(?<max>[0-9.]+)\b}xms) {
|
|
$mapping->{$okey}->{$ikey->{$_}->{type}}->{maxVal} = $+{max};
|
|
$mapping->{$okey}->{$ikey->{$_}->{type}}->{minVal} = $+{min};
|
|
$mapping->{$okey}->{$ikey->{$_}->{type}}->{step} = $+{step};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $mapping;
|
|
}
|
|
|
|
sub perlExecute {
|
|
my $hash = shift // return;
|
|
my $device = shift;
|
|
my $cmd = shift;
|
|
my $value = shift;
|
|
my $siteId = shift // $hash->{defaultRoom};
|
|
$siteId = $hash->{defaultRoom} if $siteId eq 'default';
|
|
|
|
# Nutzervariablen setzen
|
|
my %specials = (
|
|
'$DEVICE' => $device,
|
|
'$VALUE' => $value,
|
|
'$ROOM' => $siteId
|
|
);
|
|
|
|
$cmd = EvalSpecials($cmd, %specials);
|
|
|
|
# CMD ausführen
|
|
return AnalyzePerlCommand( $hash, $cmd );
|
|
}
|
|
|
|
sub RHASSPY_Confirmation {
|
|
my $hash = shift // return;
|
|
my $mode = shift; #undef => timeout, 1 => cancellation, #2 => set timer
|
|
my $data = shift // $hash->{helper}{'.delayed'};
|
|
my $timeout = shift;
|
|
my $response = shift;
|
|
|
|
#timeout Case
|
|
if (!defined $mode) {
|
|
RemoveInternalTimer( $hash, \&RHASSPY_Confirmation );
|
|
$response = $hash->{helper}{lng}->{responses}->{DefaultConfirmationTimeout};
|
|
#Beta-User: we may need to start a new session first?
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
delete $hash->{helper}{'.delayed'};
|
|
initialize_DialogManager($hash);
|
|
return;
|
|
}
|
|
|
|
#cancellation Case
|
|
if ( $mode == 1 ) {
|
|
RemoveInternalTimer( $hash, \&RHASSPY_Confirmation );
|
|
$response = $hash->{helper}{lng}->{responses}->{ defined $hash->{helper}{'.delayed'} ? 'DefaultCancelConfirmation' : 'SilentCancelConfirmation' };
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
delete $hash->{helper}{'.delayed'};
|
|
initialize_DialogManager($hash);
|
|
return $hash->{NAME};
|
|
}
|
|
if ( $mode == 2 ) {
|
|
RemoveInternalTimer( $hash, \&RHASSPY_Confirmation );
|
|
$hash->{helper}{'.delayed'} = $data;
|
|
$response = $hash->{helper}{lng}->{responses}->{DefaultConfirmationReceived} if $response eq 'default';
|
|
|
|
InternalTimer(time + $timeout, \&RHASSPY_Confirmation, $hash, 0);
|
|
|
|
#interactive dialogue as described in https://rhasspy.readthedocs.io/en/latest/reference/#dialoguemanager_continuesession and https://docs.snips.ai/articles/platform/dialog/multi-turn-dialog
|
|
my $ca_string = qq{$hash->{LANGUAGE}.$hash->{fhemId}:ConfirmAction};
|
|
my $reaction = { text => $response,
|
|
intentFilter => ["$ca_string"] };
|
|
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $reaction);
|
|
initialize_DialogManager($hash);
|
|
|
|
my $toTrigger = $hash->{'.toTrigger'} // $hash->{NAME};
|
|
delete $hash->{'.toTrigger'};
|
|
|
|
return $toTrigger;
|
|
}
|
|
|
|
return $hash->{NAME};
|
|
}
|
|
|
|
|
|
#from https://stackoverflow.com/a/43873983, modified...
|
|
sub get_unique {
|
|
my $arr = shift;
|
|
my $sorted = shift; #true if shall be sorted (longest first!)
|
|
|
|
my @unique = uniq @{$arr};
|
|
return if !@unique;
|
|
|
|
return @unique if !$sorted;
|
|
|
|
my @sorted = sort { length($b) <=> length($a) } @unique;
|
|
#Log3(undef, 5, "get_unique sorted to ".join q{ }, @sorted);
|
|
return @sorted;
|
|
}
|
|
|
|
#small function to replace variables
|
|
sub _replace {
|
|
my $hash = shift // return;
|
|
my $cmd = shift // return;
|
|
my $hash2 = shift;
|
|
my $self = $hash2->{'$SELF'} // $hash->{NAME};
|
|
my $name = $hash2->{'$NAME'} // $hash->{NAME};
|
|
my $parent = ( caller(1) )[3];
|
|
Log3($hash->{NAME}, 5, "_replace from $parent starting with: $cmd");
|
|
|
|
my %specials = (
|
|
'$SELF' => $self,
|
|
'$NAME' => $name
|
|
);
|
|
%specials = (%specials, %{$hash2});
|
|
for my $key (keys %specials) {
|
|
my $val = $specials{$key};
|
|
$cmd =~ s{\Q$key\E}{$val}gxms;
|
|
}
|
|
Log3($hash->{NAME}, 5, "_replace from $parent returns: $cmd");
|
|
return $cmd;
|
|
}
|
|
|
|
#based on compareHashes https://stackoverflow.com/a/56128395
|
|
sub _combineHashes {
|
|
my ($hash1, $hash2, $parent) = @_;
|
|
my $hash3 = {};
|
|
|
|
for my $key (keys %{$hash1}) {
|
|
$hash3->{$key} = $hash1->{$key};
|
|
if (!exists $hash2->{$key}) {
|
|
next;
|
|
}
|
|
if ( ref $hash3->{$key} eq 'HASH' and ref $hash2->{$key} eq 'HASH' ) {
|
|
$hash3->{$key} = _combineHashes($hash3->{$key}, $hash2->{$key}, $key);
|
|
} elsif ( !ref $hash3->{$key} && !ref $hash2->{$key} ) {
|
|
$hash3->{$key} = $hash2->{$key};
|
|
}
|
|
}
|
|
for (qw(commaconversion mutated_vowels)) {
|
|
$hash3->{$_} = $hash2->{$_} if defined $hash2->{$_};
|
|
}
|
|
return $hash3;
|
|
}
|
|
|
|
# derived from structure_asyncQueue
|
|
sub RHASSPY_asyncQueue {
|
|
my $hash = shift // return;
|
|
my $next_cmd = shift @{$hash->{'.asyncQueue'}};
|
|
if (defined $next_cmd) {
|
|
analyzeAndRunCmd($hash, $next_cmd->{device}, $next_cmd->{cmd}) if defined $next_cmd->{cmd};
|
|
handleIntentSetNumeric($hash, $next_cmd->{SetNumeric}) if defined $next_cmd->{SetNumeric};
|
|
my $async_delay = $next_cmd->{delay} // 0;
|
|
InternalTimer(time+$async_delay,\&RHASSPY_asyncQueue,$hash,0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
sub _sortAsyncQueue {
|
|
my $hash = shift // return;
|
|
my $queue = @{$hash->{'.asyncQueue'}};
|
|
|
|
my @devlist = sort {
|
|
$a->{prio} <=> $b->{prio}
|
|
or
|
|
$a->{delay} <=> $b->{delay}
|
|
} @{$queue};
|
|
$hash->{'.asyncQueue'} = @devlist;
|
|
return;
|
|
}
|
|
|
|
# Get all devicenames with Rhasspy relevance
|
|
sub getAllRhasspyNames {
|
|
my $hash = shift // return;
|
|
return if !defined $hash->{helper}{devicemap};
|
|
|
|
my @devices;
|
|
my $rRooms = $hash->{helper}{devicemap}{rhasspyRooms};
|
|
for my $key (keys %{$rRooms}) {
|
|
push @devices, keys %{$rRooms->{$key}};
|
|
}
|
|
return get_unique(\@devices, 1 );
|
|
}
|
|
|
|
# Alle Raumbezeichnungen sammeln
|
|
sub getAllRhasspyRooms {
|
|
my $hash = shift // return;
|
|
return keys %{$hash->{helper}{devicemap}{rhasspyRooms}} if defined $hash->{helper}{devicemap};
|
|
return;
|
|
}
|
|
|
|
|
|
# Alle Sender sammeln
|
|
sub getAllRhasspyChannels {
|
|
my $hash = shift // return;
|
|
return if !defined $hash->{helper}{devicemap};
|
|
|
|
my @channels;
|
|
for my $room (keys %{$hash->{helper}{devicemap}{Channels}}) {
|
|
push @channels, keys %{$hash->{helper}{devicemap}{Channels}{$room}}
|
|
}
|
|
return get_unique(\@channels, 1 );
|
|
}
|
|
|
|
|
|
# Collect all NumericTypes
|
|
sub getAllRhasspyTypes {
|
|
my $hash = shift // return;
|
|
return if !defined $hash->{helper}{devicemap};
|
|
|
|
my @types;
|
|
for my $dev (keys %{$hash->{helper}{devicemap}{devices}}) {
|
|
for my $intent (keys %{$hash->{helper}{devicemap}{devices}{$dev}{intents}}) {
|
|
my $type;
|
|
$type = $hash->{helper}{devicemap}{devices}{$dev}{intents}{$intent};
|
|
push @types, keys %{$type} if $intent =~ m{\A[GS]etNumeric}x;
|
|
}
|
|
}
|
|
return get_unique(\@types, 1 );
|
|
}
|
|
|
|
|
|
# Collect all clours
|
|
sub getAllRhasspyColors {
|
|
my $hash = shift // return;
|
|
return if !defined $hash->{helper}{devicemap};
|
|
|
|
my @colors;
|
|
for my $room (keys %{$hash->{helper}{devicemap}{Colors}}) {
|
|
push @colors, keys %{$hash->{helper}{devicemap}{Colors}{$room}}
|
|
}
|
|
return get_unique(\@colors, 1 );
|
|
}
|
|
|
|
|
|
# get a list of all used groups
|
|
sub getAllRhasspyGroups {
|
|
my $hash = shift // return;
|
|
my @groups;
|
|
|
|
for my $device (keys %{$hash->{helper}{devicemap}{devices}}) {
|
|
my $devgroups = $hash->{helper}{devicemap}{devices}{$device}->{groups} // q{};;
|
|
#for (@{$devgroups}) {
|
|
for (split m{,}xi, $devgroups ) {
|
|
push @groups, $_;
|
|
}
|
|
}
|
|
return get_unique(\@groups, 1);
|
|
}
|
|
|
|
# Derive room info from spoken text, siteId or additional logics around siteId
|
|
sub getRoomName {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
# Slot "Room" in JSON? Otherwise use info from used satellite
|
|
return $data->{Room} if exists($data->{Room});
|
|
|
|
my $room;
|
|
|
|
#Beta-User: This might be the right place to check, if there's additional logic implemented...
|
|
|
|
my $siteId = $data->{siteId};
|
|
|
|
my $rreading = makeReadingName("siteId2room_$siteId");
|
|
$siteId =~ s{\A([^.]+).*}{$1}xms;
|
|
utf8::downgrade($siteId, 1);
|
|
$room = ReadingsVal($hash->{NAME}, $rreading, lc $siteId);
|
|
#$room = ReadingsVal($hash->{NAME}, $rreading, $siteId);
|
|
$room = $hash->{defaultRoom} if $room eq 'default' || !(length $room);
|
|
Log3($hash->{NAME}, 5, "room is identified using siteId as $room");
|
|
|
|
return $room;
|
|
}
|
|
|
|
|
|
# Gerät über Raum und Namen suchen.
|
|
sub getDeviceByName {
|
|
my $hash = shift // return;
|
|
my $room = shift;
|
|
my $name = shift; #either of the two required
|
|
|
|
return if !$room && !$name;
|
|
|
|
my $device;
|
|
|
|
return if !defined $hash->{helper}{devicemap};
|
|
|
|
$device = $hash->{helper}{devicemap}{rhasspyRooms}{$room}{$name};
|
|
#return $device if $device;
|
|
if ($device) {
|
|
Log3($hash->{NAME}, 5, "Device selected (by hash, with room and name): $device");
|
|
return $device ;
|
|
}
|
|
for (keys %{$hash->{helper}{devicemap}{rhasspyRooms}}) {
|
|
$device = $hash->{helper}{devicemap}{rhasspyRooms}{$_}{$name};
|
|
#return $device if $device;
|
|
if ($device) {
|
|
Log3($hash->{NAME}, 5, "Device selected (by hash, using only name): $device");
|
|
return $device ;
|
|
}
|
|
}
|
|
Log3($hash->{NAME}, 1, "No device for >>$name<< found, especially not in room >>$room<< (also not outside)!");
|
|
return;
|
|
}
|
|
|
|
|
|
# returns lists of "might be relevant" devices via room, intent and (optional) Type info
|
|
sub getDevicesByIntentAndType {
|
|
my $hash = shift // return;
|
|
my $room = shift;
|
|
my $intent = shift;
|
|
my $type = shift; #Beta-User: any necessary parameters...?
|
|
my $subType = shift // $type;
|
|
|
|
my @matchesInRoom; my @matchesOutsideRoom;
|
|
|
|
return if !defined $hash->{helper}{devicemap};
|
|
for my $devs (keys %{$hash->{helper}{devicemap}{devices}}) {
|
|
my $mapping = getMapping($hash, $devs, $intent, { type => $type, subType => $subType }, 1, 1) // next;
|
|
my $mappingType = $mapping->{type};
|
|
my $rooms = $hash->{helper}{devicemap}{devices}{$devs}->{rooms};
|
|
|
|
# get lists of devices that may fit to requirements
|
|
if ( !defined $type ) {
|
|
$rooms =~ m{\b$room\b}ix
|
|
? push @matchesInRoom, $devs
|
|
: push @matchesOutsideRoom, $devs;
|
|
}
|
|
elsif ( defined $type && $mappingType && $type =~ m{\A$mappingType\z}ix ) {
|
|
$rooms =~ m{\b$room\b}ix
|
|
? push @matchesInRoom, $devs
|
|
: push @matchesOutsideRoom, $devs;
|
|
}
|
|
}
|
|
return (\@matchesInRoom, \@matchesOutsideRoom);
|
|
}
|
|
|
|
# Identify single device via room, intent and (optional) Type info
|
|
sub getDeviceByIntentAndType {
|
|
my $hash = shift // return;
|
|
my $room = shift;
|
|
my $intent = shift;
|
|
my $type = shift; #Beta-User: any necessary parameters...?
|
|
#my $inBulk = shift // 0;
|
|
|
|
#rem. Beta-User: atm function is only called by GetNumeric!
|
|
my $device;
|
|
|
|
# Devices sammeln
|
|
my ($matchesInRoom, $matchesOutsideRoom) = getDevicesByIntentAndType($hash, $room, $intent, $type);
|
|
Log3($hash->{NAME}, 5, "matches in room: @{$matchesInRoom}, matches outside: @{$matchesOutsideRoom}");
|
|
|
|
# Erstes Device im passenden Raum zurückliefern falls vorhanden, sonst erstes Device außerhalb
|
|
$device = (@{$matchesInRoom}) ? shift @{$matchesInRoom} : shift @{$matchesOutsideRoom};
|
|
|
|
Log3($hash->{NAME}, 5, "Device selected: ". $device ? $device : "none");
|
|
|
|
return $device;
|
|
}
|
|
|
|
|
|
# Eingeschaltetes Gerät mit bestimmten Intent und optional Type suchen
|
|
sub getActiveDeviceForIntentAndType {
|
|
my $hash = shift // return;
|
|
my $room = shift;
|
|
my $intent = shift;
|
|
my $type = shift; #Beta-User: any necessary parameters...?
|
|
|
|
my $device;
|
|
my ($matchesInRoom, $matchesOutsideRoom) = getDevicesByIntentAndType($hash, $room, $intent, $type);
|
|
|
|
# Anonyme Funktion zum finden des aktiven Geräts
|
|
my $activeDevice = sub ($$) {
|
|
my $subhash = shift;
|
|
my $devices = shift // return;
|
|
my $match;
|
|
|
|
for (@{$devices}) {
|
|
my $mapping = getMapping($subhash, $_, 'GetOnOff', undef, defined $hash->{helper}{devicemap}, 1);
|
|
if (defined $mapping ) {
|
|
# Gerät ein- oder ausgeschaltet?
|
|
my $value = _getOnOffState($subhash, $_, $mapping);
|
|
if ($value) {
|
|
$match = $_;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
return $match;
|
|
};
|
|
|
|
# Gerät finden, erst im aktuellen Raum, sonst in den restlichen
|
|
$device = $activeDevice->($hash, $matchesInRoom);
|
|
$device = $activeDevice->($hash, $matchesOutsideRoom) if !defined $device;
|
|
|
|
Log3($hash->{NAME}, 5, "Device selected: $device");
|
|
|
|
return $device;
|
|
}
|
|
|
|
|
|
# Gerät mit bestimmtem Sender suchen
|
|
sub getDeviceByMediaChannel {
|
|
my $hash = shift // return;
|
|
my $room = shift;
|
|
my $channel = shift; #Beta-User: any necessary parameters...?
|
|
|
|
my $device;
|
|
|
|
return if !defined $hash->{helper}{devicemap};
|
|
my $devices = $hash->{helper}{devicemap}{Channels}{$room}->{$channel};
|
|
$device = ${$devices}[0];
|
|
if ($device) {
|
|
Log3($hash->{NAME}, 5, "Device selected (by hash, with room and channel): $device");
|
|
return $device ;
|
|
}
|
|
for (sort keys %{$hash->{helper}{devicemap}{Channels}}) {
|
|
$devices = $hash->{helper}{devicemap}{Channels}{$_}{$channel};
|
|
$device = ${$devices}[0];
|
|
|
|
#return $device if $device;
|
|
if ($device) {
|
|
Log3($hash->{NAME}, 5, "Device selected (by hash, using only channel): $device");
|
|
return $device ;
|
|
}
|
|
}
|
|
Log3($hash->{NAME}, 1, "No device for >>$channel<< found, especially not in room >>$room<< (also not outside)!");
|
|
return;
|
|
}
|
|
|
|
sub getDevicesByGroup {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
my $group = $data->{Group} // return;
|
|
my $room = $data->{Room} // return;
|
|
|
|
my $devices = {};
|
|
|
|
for my $dev (keys %{$hash->{helper}{devicemap}{devices}}) {
|
|
my $allrooms = $hash->{helper}{devicemap}{devices}{$dev}->{rooms};
|
|
next if $room ne 'global' && $allrooms !~ m{\b$room\b}x;
|
|
|
|
my $allgroups = $hash->{helper}{devicemap}{devices}{$dev}->{groups} // next;
|
|
next if $allgroups !~ m{\b$group\b}x;
|
|
|
|
my $specials = $hash->{helper}{devicemap}{devices}{$dev}{group_specials};
|
|
my $label = $specials->{partOf} // $dev;
|
|
next if defined $devices->{$label};
|
|
|
|
my $delay = $specials->{async_delay} // 0;
|
|
my $prio = $specials->{prio} // 0;
|
|
$devices->{$label} = { delay => $delay, prio => $prio };
|
|
}
|
|
return $devices;
|
|
}
|
|
|
|
|
|
# Mappings in Key/Value Paare aufteilen
|
|
sub splitMappingString {
|
|
my $mapping = shift // return;
|
|
my @tokens; my $token = q{};
|
|
#my $char,
|
|
my $lastChar = q{};
|
|
my $bracketLevel = 0;
|
|
my %parsedMapping;
|
|
|
|
# String in Kommagetrennte Tokens teilen
|
|
for my $char ( split q{}, $mapping ) {
|
|
if ($char eq q<{> && $lastChar ne '\\') {
|
|
$bracketLevel += 1;
|
|
$token .= $char;
|
|
}
|
|
elsif ($char eq q<}> && $lastChar ne '\\') {
|
|
$bracketLevel -= 1;
|
|
$token .= $char;
|
|
}
|
|
elsif ($char eq ',' && $lastChar ne '\\' && !$bracketLevel) {
|
|
push(@tokens, $token);
|
|
$token = q{};
|
|
}
|
|
else {
|
|
$token .= $char;
|
|
}
|
|
|
|
$lastChar = $char;
|
|
}
|
|
push @tokens, $token if length $token;
|
|
|
|
# Tokens in Keys/Values trennen
|
|
%parsedMapping = map {split m{=}x, $_, 2} @tokens; #Beta-User: Odd number of elements in hash assignment
|
|
|
|
return %parsedMapping;
|
|
}
|
|
|
|
|
|
# rhasspyMapping parsen und gefundene Settings zurückliefern
|
|
sub getMapping {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
my $intent = shift // return;
|
|
my $type = shift // $intent; #Beta-User: seems first three parameters are obligatory...?
|
|
my $fromHash = shift // 0;
|
|
my $disableLog = shift // 0;
|
|
|
|
my $subType = $type;
|
|
if (ref $type eq 'HASH') {
|
|
$subType = $type->{subType};
|
|
$type = $type->{type};
|
|
}
|
|
|
|
my $matchedMapping;
|
|
|
|
if ($fromHash) {
|
|
$matchedMapping = $hash->{helper}{devicemap}{devices}{$device}{intents}{$intent}{$subType};
|
|
return $matchedMapping if $matchedMapping;
|
|
|
|
for (sort keys %{$hash->{helper}{devicemap}{devices}{$device}{intents}{$intent}}) {
|
|
#simply pick first item in alphabetical order...
|
|
return $hash->{helper}{devicemap}{devices}{$device}{intents}{$intent}{$_};
|
|
}
|
|
}
|
|
|
|
my $prefix = $hash->{prefix};
|
|
my $mappingsString = AttrVal($device, "${prefix}Mapping", undef) // return;
|
|
|
|
for (split m{\n}x, $mappingsString) {
|
|
|
|
# Nur Mappings vom gesuchten Typ verwenden
|
|
next if $_ !~ qr/^$intent/x;
|
|
$_ =~ s/$intent://x;
|
|
my %currentMapping = splitMappingString($_);
|
|
|
|
# Erstes Mapping vom passenden Intent wählen (unabhängig vom Type), dann ggf. weitersuchen ob noch ein besserer Treffer mit passendem Type kommt
|
|
if (!defined $matchedMapping
|
|
|| lc($matchedMapping->{type}) ne lc($type) && lc($currentMapping{type}) eq lc($type)
|
|
|| $de_mappings->{ToEn}->{$matchedMapping->{type}} ne $type && $de_mappings->{ToEn}->{$currentMapping{type}} eq $type
|
|
) {
|
|
$matchedMapping = \%currentMapping;
|
|
#Beta-User: könnte man ergänzen durch den match "vorne" bei Reading, kann aber sein, dass es effektiver geht, wenn wir das künftig sowieso anders machen...
|
|
|
|
Log3($hash->{NAME}, 5, "${prefix}Mapping selected: $_") if !$disableLog;
|
|
}
|
|
}
|
|
return $matchedMapping;
|
|
}
|
|
|
|
|
|
# Cmd von Attribut mit dem Format value=cmd pro Zeile lesen
|
|
sub getKeyValFromAttr {
|
|
my $hash = shift // return;
|
|
my $device = shift;
|
|
my $reading = shift;
|
|
my $key = shift; #Beta-User: any necessary parameters...?
|
|
my $disableLog = shift // 0;
|
|
|
|
my $cmd;
|
|
|
|
# String in einzelne Mappings teilen
|
|
my @rows = split(m{\n}x, AttrVal($device, $reading, q{}));
|
|
|
|
for (@rows) {
|
|
# Nur Zeilen mit gesuchten Identifier verwenden
|
|
next if $_ !~ qr/^$key=/ix;
|
|
$_ =~ s{$key=}{}ix;
|
|
$cmd = $_;
|
|
|
|
Log3($hash->{NAME}, 5, "cmd selected: $_") if !$disableLog;
|
|
last;
|
|
}
|
|
|
|
return $cmd;
|
|
}
|
|
|
|
# Cmd String im Format 'cmd', 'device:cmd', 'fhemcmd1; fhemcmd2' oder '{<perlcode}' ausführen
|
|
sub analyzeAndRunCmd {
|
|
my $hash = shift // return;
|
|
my $device = shift;
|
|
my $cmd = shift;
|
|
my $val = shift;
|
|
my $siteId = shift // $hash->{defaultRoom};
|
|
my $error;
|
|
my $returnVal;
|
|
$siteId = $hash->{defaultRoom} if $siteId eq 'default';
|
|
|
|
Log3($hash->{NAME}, 5, "analyzeAndRunCmd called with command: $cmd");
|
|
|
|
# Perl Command
|
|
if ($cmd =~ m{\A\s*\{.*\}\s*\z}x) { #escaping closing bracket for editor only
|
|
# CMD ausführen
|
|
Log3($hash->{NAME}, 5, "$cmd is a perl command");
|
|
return perlExecute($hash, $device, $cmd, $val,$siteId);
|
|
}
|
|
|
|
# String in Anführungszeichen (mit ReplaceSetMagic)
|
|
if ($cmd =~ m{\A\s*"(?<inner>.*)"\s*\z}x) {
|
|
my $DEVICE = $device;
|
|
my $ROOM = $siteId;
|
|
my $VALUE = $val;
|
|
|
|
Log3($hash->{NAME}, 5, "$cmd has quotes...");
|
|
|
|
# Anführungszeichen entfernen
|
|
$cmd = $+{inner} // q{};
|
|
|
|
# Variablen ersetzen?
|
|
if ( !eval { $cmd =~ s{(\$\w+)}{$1}eegx; 1 } ) {
|
|
Log3($hash->{NAME}, 1, "$cmd returned Error: $@");
|
|
return;
|
|
}
|
|
# [DEVICE:READING] Einträge ersetzen
|
|
$returnVal = _ReplaceReadingsVal($hash, $cmd);
|
|
# Escapte Kommas wieder durch normale ersetzen
|
|
$returnVal =~ s{\\,}{,}x;
|
|
Log3($hash->{NAME}, 5, "...and is now: $cmd ($returnVal)");
|
|
}
|
|
# FHEM Command oder CommandChain
|
|
elsif (defined $cmds{ (split m{\s+}x, $cmd)[0] }) {
|
|
#my @test = split q{ }, $cmd;
|
|
Log3($hash->{NAME}, 5, "$cmd is a FHEM command");
|
|
$error = AnalyzeCommandChain($hash, $cmd);
|
|
$returnVal = (split m{\s+}x, $cmd)[1];
|
|
}
|
|
# Soll Command auf anderes Device umgelenkt werden?
|
|
elsif ($cmd =~ m{:}x) {
|
|
$cmd =~ s{:}{ }x;
|
|
$cmd = qq($cmd $val) if defined($val);
|
|
Log3($hash->{NAME}, 5, "$cmd redirects to another device");
|
|
$error = AnalyzeCommand($hash, "set $cmd");
|
|
$returnVal = (split q{ }, $cmd)[1];
|
|
}
|
|
# Nur normales Cmd angegeben
|
|
else {
|
|
$cmd = qq($device $cmd);
|
|
$cmd = qq($cmd $val) if defined $val;
|
|
Log3($hash->{NAME}, 5, "$cmd is a normal command");
|
|
$error = AnalyzeCommand($hash, "set $cmd");
|
|
$returnVal = (split q{ }, $cmd)[1];
|
|
}
|
|
Log3($hash->{NAME}, 1, $_) if defined $error;
|
|
|
|
return $returnVal;
|
|
}
|
|
|
|
|
|
# Wert über Format 'reading', 'device:reading' oder '{<perlcode}' lesen
|
|
sub _getValue {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
my $getString = shift // return;
|
|
my $val = shift;
|
|
my $siteId = shift;
|
|
|
|
# Perl Command oder in Anführungszeichen? -> Umleiten zu analyzeAndRunCmd
|
|
if ($getString =~ m{\A\s*\{.*\}\s*\z}x || $getString =~ m{\A\s*".*"\s*\z}x) {
|
|
return analyzeAndRunCmd($hash, $device, $getString, $val, $siteId);
|
|
}
|
|
|
|
# Soll Reading von einem anderen Device gelesen werden?
|
|
if ($getString =~ m{:}x) {
|
|
$getString =~ s{\[([^]]+)]}{$1}x; #remove brackets
|
|
my @replace = split m{:}x, $getString;
|
|
$device = $replace[0];
|
|
$getString = $replace[1] // $getString;
|
|
return ReadingsVal($device, $getString, 0);
|
|
}
|
|
|
|
# If it's only a string without quotes, return string for TTS
|
|
#return ReadingsVal($device, $getString, $getString);
|
|
return ReadingsVal($device, $getString, $getString);
|
|
}
|
|
|
|
|
|
# Zustand eines Gerätes über GetOnOff Mapping abfragen
|
|
sub _getOnOffState {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
my $mapping = shift // return;
|
|
|
|
my $valueOn = $mapping->{valueOn};
|
|
my $valueOff = $mapping->{valueOff};
|
|
my $value = lc(_getValue($hash, $device, $mapping->{currentVal}));
|
|
|
|
# Entscheiden ob $value 0 oder 1 ist
|
|
if ( defined $valueOff ) {
|
|
$value eq lc($valueOff) ? return 0 : return 1;
|
|
}
|
|
if ( defined $valueOn ) {
|
|
$value eq lc($valueOn) ? return 1 : return 0;
|
|
}
|
|
|
|
# valueOn und valueOff sind nicht angegeben worden, alles außer "off" wird als eine 1 gewertet
|
|
return $value eq 'off' ? 0 : 1;
|
|
}
|
|
|
|
|
|
# JSON parsen
|
|
sub parseJSONPayload {
|
|
my $hash = shift;
|
|
my $json = shift // return;
|
|
my $data;
|
|
my $cp = $hash->{encoding} // q{UTF-8};
|
|
|
|
# JSON Decode und Fehlerüberprüfung
|
|
my $decoded;
|
|
if ( !eval { $decoded = decode_json(encode($cp,$json)) ; 1 } ) {
|
|
return Log3($hash->{NAME}, 1, "JSON decoding error: $@");
|
|
}
|
|
|
|
# Standard-Keys auslesen
|
|
($data->{intent} = $decoded->{intent}{intentName}) =~ s{\A.*.:}{}x if exists $decoded->{intent}{intentName};
|
|
$data->{probability} = $decoded->{intent}{confidenceScore} if exists $decoded->{intent}{confidenceScore};
|
|
$data->{sessionId} = $decoded->{sessionId} if exists $decoded->{sessionId};
|
|
$data->{siteId} = $decoded->{siteId} if exists $decoded->{siteId};
|
|
$data->{input} = $decoded->{input} if exists $decoded->{input};
|
|
$data->{rawInput} = $decoded->{rawInput} if exists $decoded->{rawInput};
|
|
|
|
|
|
# Überprüfen ob Slot Array existiert
|
|
if (exists $decoded->{slots}) {
|
|
# Key -> Value Paare aus dem Slot Array ziehen
|
|
for my $slot (@{$decoded->{slots}}) {
|
|
my $slotName = $slot->{slotName};
|
|
my $slotValue;
|
|
|
|
$slotValue = $slot->{value}{value} if exists $slot->{value}{value} && $slot->{value}{value} ne '';#Beta-User: dismiss effectively empty fields
|
|
$slotValue = $slot->{value} if exists $slot->{entity} && $slot->{entity} eq 'rhasspy/duration';
|
|
|
|
$data->{$slotName} = $slotValue;
|
|
}
|
|
}
|
|
|
|
for (keys %{ $data }) {
|
|
my $value = $data->{$_};
|
|
Log3($hash->{NAME}, 5, "Parsed value: $value for key: $_");
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
# Call von IODev-Dispatch (e.g.MQTT2)
|
|
sub Parse {
|
|
my $iodev = shift // carp q[No IODev provided!] && return;
|
|
my $msg = shift // carp q[No message to analyze!] && return;
|
|
|
|
my $ioname = $iodev->{NAME};
|
|
$msg =~ s{\Aautocreate=([^\0]+)\0(.*)\z}{$2}sx;
|
|
my ($cid, $topic, $value) = split m{\0}xms, $msg, 3;
|
|
my @ret=();
|
|
my $forceNext = 0;
|
|
my $shorttopic = $topic =~ m{([^/]+/[^/]+/)}x ? $1 : return q{[NEXT]};
|
|
|
|
return q{[NEXT]} if !grep( { m{\A$shorttopic}x } @topics);
|
|
|
|
my @instances = devspec2array('TYPE=RHASSPY');
|
|
|
|
for my $dev (@instances) {
|
|
my $hash = $defs{$dev};
|
|
# Name mit IODev vergleichen
|
|
next if $ioname ne AttrVal($hash->{NAME}, 'IODev', undef);
|
|
next if IsDisabled( $hash->{NAME} );
|
|
my $topicpart = qq{/$hash->{LANGUAGE}\.$hash->{fhemId}\[._]|hermes/dialogueManager};
|
|
next if $topic !~ m{$topicpart}x;
|
|
|
|
Log3($hash,5,"RHASSPY: [$hash->{NAME}] Parse (IO: ${ioname}): Msg: $topic => $value");
|
|
|
|
my $fret = analyzeMQTTmessage($hash, $topic, $value);
|
|
next if !defined $fret;
|
|
if( ref $fret eq 'ARRAY' ) {
|
|
push (@ret, @{$fret});
|
|
$forceNext = 1 if AttrVal($hash->{NAME},'forceNEXT',0);
|
|
} else {
|
|
Log3($hash->{NAME},5,"RHASSPY: [$hash->{NAME}] Parse: internal error: onmessage returned an unexpected value: ".$fret);
|
|
}
|
|
}
|
|
unshift(@ret, '[NEXT]') if !(@ret) || $forceNext;
|
|
#Log3($iodev, 4, "Parse collected these devices: ". join q{ },@ret);
|
|
return @ret;
|
|
}
|
|
|
|
# Update the readings lastIntentPayload and lastIntentTopic
|
|
# after and intent is received
|
|
sub updateLastIntentReadings {
|
|
my $hash = shift;
|
|
my $topic = shift;
|
|
my $data = shift // return;
|
|
|
|
readingsBeginUpdate($hash);
|
|
readingsBulkUpdate($hash, 'lastIntentTopic', $topic);
|
|
readingsBulkUpdate($hash, 'lastIntentPayload', toJSON($data));
|
|
readingsEndUpdate($hash, 1);
|
|
return;
|
|
}
|
|
|
|
|
|
#Make globally available to allow later use by other functions, esp. handleIntentConfirmAction
|
|
my $dispatchFns = {
|
|
Shortcuts => \&handleIntentShortcuts,
|
|
SetOnOff => \&handleIntentSetOnOff,
|
|
SetOnOffGroup => \&handleIntentSetOnOffGroup,
|
|
GetOnOff => \&handleIntentGetOnOff,
|
|
SetNumeric => \&handleIntentSetNumeric,
|
|
SetNumericGroup => \&handleIntentSetNumericGroup,
|
|
GetNumeric => \&handleIntentGetNumeric,
|
|
GetState => \&handleIntentGetState,
|
|
MediaControls => \&handleIntentMediaControls,
|
|
MediaChannels => \&handleIntentMediaChannels,
|
|
SetColor => \&handleIntentSetColor,
|
|
SetColorGroup => \&handleIntentSetColorGroup,
|
|
GetTime => \&handleIntentGetTime,
|
|
GetWeekday => \&handleIntentGetWeekday,
|
|
SetTimer => \&handleIntentSetTimer,
|
|
ConfirmAction => \&handleIntentConfirmAction,
|
|
ReSpeak => \&handleIntentReSpeak
|
|
};
|
|
|
|
|
|
# Daten vom MQTT Modul empfangen -> Device und Room ersetzen, dann erneut an NLU übergeben
|
|
sub analyzeMQTTmessage {
|
|
my $hash = shift;# // return;
|
|
my $topic = shift;# // carp q[No topic provided!] && return;
|
|
my $message = shift;# // carp q[No message provided!] && return;;
|
|
|
|
my $data = parseJSONPayload($hash, $message);
|
|
my $fhemId = $hash->{fhemId};
|
|
|
|
my $input = $data->{input};
|
|
|
|
my $device;
|
|
my @updatedList;
|
|
|
|
my $type = $data->{type} // q{text};
|
|
my $sessionId = $data->{sessionId};
|
|
my $siteId = $data->{siteId};
|
|
my $mute = 0;
|
|
|
|
if (defined $siteId) {
|
|
my $reading = makeReadingName($siteId);
|
|
$mute = ReadingsNum($hash->{NAME},"mute_$reading",0);
|
|
}
|
|
|
|
# Hotword detection
|
|
if ($topic =~ m{\Ahermes/dialogueManager}x) {
|
|
my $room = getRoomName($hash, $data);
|
|
|
|
return if !defined $room;
|
|
my $mutated_vowels = $hash->{helper}{lng}->{mutated_vowels};
|
|
if (defined $mutated_vowels) {
|
|
for (keys %{$mutated_vowels}) {
|
|
$room =~ s{$_}{$mutated_vowels->{$_}}gx;
|
|
}
|
|
}
|
|
|
|
if ( $topic =~ m{sessionStarted}x ) {
|
|
readingsSingleUpdate($hash, "listening_" . makeReadingName($room), 1, 1);
|
|
} elsif ( $topic =~ m{sessionEnded}x ) {
|
|
readingsSingleUpdate($hash, 'listening_' . makeReadingName($room), 0, 1);
|
|
}
|
|
push @updatedList, $hash->{NAME};
|
|
return \@updatedList;
|
|
}
|
|
|
|
if ($topic =~ m{\Ahermes/intent/.*[:_]SetMute}x && defined $siteId) {
|
|
$type = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice';
|
|
$data->{requestType} = $type;
|
|
|
|
# update Readings
|
|
updateLastIntentReadings($hash, $topic,$data);
|
|
handleIntentSetMute($hash, $data);
|
|
push @updatedList, $hash->{NAME};
|
|
return \@updatedList;
|
|
}
|
|
|
|
if ($mute) {
|
|
$data->{requestType} = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice';
|
|
respond($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, q{ });
|
|
#Beta-User: Da fehlt mir der Soll-Ablauf für das "room-listening"-Reading; das wird ja über einen anderen Topic abgewickelt
|
|
return \@updatedList;
|
|
}
|
|
|
|
my $command = $data->{input};
|
|
$type = $message =~ m{${fhemId}.textCommand}x ? 'text' : 'voice';
|
|
$data->{requestType} = $type;
|
|
my $intent = $data->{intent};
|
|
|
|
# update Readings
|
|
updateLastIntentReadings($hash, $topic,$data);
|
|
|
|
# Passenden Intent-Handler aufrufen
|
|
if (ref $dispatchFns->{$intent} eq 'CODE') {
|
|
$device = $dispatchFns->{$intent}->($hash, $data);
|
|
} else {
|
|
$device = handleCustomIntent($hash, $intent, $data);
|
|
}
|
|
|
|
my $name = $hash->{NAME};
|
|
$device = $device // $name;
|
|
$device .= ",$name" if $device !~ m{$name}x;
|
|
my @candidates = split m{,}x, $device;
|
|
for (@candidates) {
|
|
push @updatedList, $_ if $defs{$_};
|
|
}
|
|
|
|
return \@updatedList;
|
|
}
|
|
|
|
|
|
# Antwort ausgeben
|
|
sub respond {
|
|
my $hash = shift // return;
|
|
my $type = shift // return;
|
|
my $sessionId = shift // return;
|
|
my $siteId = shift // return;
|
|
my $response = shift // return;
|
|
|
|
my $topic = q{endSession};
|
|
|
|
my $sendData = {
|
|
sessionId => $sessionId,
|
|
siteId => $siteId
|
|
};
|
|
|
|
if (ref $response eq 'HASH') {
|
|
#intentFilter
|
|
$topic = q{continueSession};
|
|
for my $key (keys %{$response}) {
|
|
$sendData->{$key} = $response->{$key};
|
|
}
|
|
} else {
|
|
$sendData->{text} = $response
|
|
}
|
|
|
|
my $json = toJSON($sendData);
|
|
|
|
readingsBeginUpdate($hash);
|
|
$type eq 'voice' ?
|
|
readingsBulkUpdate($hash, 'voiceResponse', $response)
|
|
: readingsBulkUpdate($hash, 'textResponse', $response);
|
|
readingsBulkUpdate($hash, 'responseType', $type);
|
|
readingsEndUpdate($hash,1);
|
|
IOWrite($hash, 'publish', qq{hermes/dialogueManager/$topic $json});
|
|
Log3($hash->{NAME}, 5, "Response is: $response");
|
|
return;
|
|
}
|
|
|
|
|
|
# Antworttexte festlegen
|
|
sub getResponse {
|
|
my $hash = shift;
|
|
my $identifier = shift // return 'Code error! No identifier provided for getResponse!' ;
|
|
|
|
return getKeyValFromAttr($hash, $hash->{NAME}, 'response', $identifier) // $hash->{helper}{lng}->{responses}->{$identifier};
|
|
}
|
|
|
|
|
|
# Send text command to Rhasspy NLU
|
|
sub sendTextCommand {
|
|
my $hash = shift // return;
|
|
my $text = shift // return;
|
|
|
|
my $data = {
|
|
input => $text,
|
|
sessionId => "$hash->{fhemId}.textCommand"
|
|
};
|
|
my $message = toJSON($data);
|
|
|
|
# Send fake command, so it's forwarded to NLU
|
|
# my $topic2 = "hermes/intent/FHEM:TextCommand";
|
|
my $topic = q{hermes/nlu/query};
|
|
|
|
return IOWrite($hash, 'publish', qq{$topic $message});
|
|
}
|
|
|
|
|
|
# Sprachausgabe / TTS über RHASSPY
|
|
sub sendSpeakCommand {
|
|
my $hash = shift;
|
|
my $cmd = shift;
|
|
|
|
my $sendData = {
|
|
id => '0',
|
|
sessionId => '0'
|
|
};
|
|
if (ref $cmd eq 'HASH') {
|
|
return 'speak with explicite params needs siteId and text as arguments!' if !defined $cmd->{siteId} || !defined $cmd->{text};
|
|
$sendData->{siteId} = $cmd->{siteId};
|
|
$sendData->{text} = $cmd->{text};
|
|
} else { #Beta-User: might need review, as parseParams is used by default...!
|
|
my $siteId = 'default';
|
|
my $text = $cmd;
|
|
my($unnamedParams, $namedParams) = parseParams($cmd);
|
|
|
|
if (defined $namedParams->{siteId} && defined $namedParams->{text}) {
|
|
$sendData->{siteId} = $namedParams->{siteId};
|
|
$sendData->{text} = $namedParams->{text};
|
|
} else {
|
|
return 'speak needs siteId and text as arguments!';
|
|
}
|
|
}
|
|
my $json = toJSON($sendData);
|
|
return IOWrite($hash, 'publish', qq{hermes/tts/say $json});
|
|
}
|
|
|
|
# Send all devices, rooms, etc. to Rhasspy HTTP-API to update the slots
|
|
sub updateSlots {
|
|
my $hash = shift // return;
|
|
my $language = $hash->{LANGUAGE};
|
|
my $fhemId = $hash->{fhemId};
|
|
my $method = q{POST};
|
|
|
|
initialize_devicemap($hash);
|
|
my $tweaks = $hash->{helper}{tweaks}->{updateSlots};
|
|
my $noEmpty = !defined $tweaks || defined $tweaks->{noEmptySlots} && $tweaks->{noEmptySlots} != 1 ? 1 : 0;
|
|
|
|
# Collect everything and store it in arrays
|
|
my @devices = getAllRhasspyNames($hash);
|
|
my @rooms = getAllRhasspyRooms($hash);
|
|
my @channels = getAllRhasspyChannels($hash);
|
|
my @colors = getAllRhasspyColors($hash);
|
|
my @types = getAllRhasspyTypes($hash);
|
|
my @groups = getAllRhasspyGroups($hash);
|
|
my @shortcuts = keys %{$hash->{helper}{shortcuts}};
|
|
|
|
if ($noEmpty) {
|
|
@devices = ('') if !@devices;
|
|
@rooms = ('') if !@rooms;
|
|
@channels = ('') if !@channels;
|
|
@colors = ('') if !@colors;
|
|
@types = ('') if !@types;
|
|
@groups = ('') if !@groups;
|
|
@shortcuts = ('') if !@shortcuts;
|
|
}
|
|
|
|
|
|
my $deviceData;
|
|
|
|
if (@shortcuts) {
|
|
my $url = q{/api/sentences};
|
|
$deviceData =qq({"intents/${language}.${fhemId}.Shortcuts.ini":"[${language}.${fhemId}:Shortcuts]\\n);
|
|
for (@shortcuts)
|
|
{
|
|
$deviceData = $deviceData . ($_) . '\n';
|
|
}
|
|
$deviceData = $deviceData . '"}';
|
|
Log3($hash->{NAME}, 5, "Updating Rhasspy Sentences with data: $deviceData");
|
|
_sendToApi($hash, $url, $method, $deviceData);
|
|
}
|
|
|
|
# If there are any devices, rooms, etc. found, create JSON structure and send it the the API
|
|
return if !@devices && !@rooms && !@channels && !@types && !@groups;
|
|
|
|
my $json;
|
|
$deviceData = {};
|
|
my $overwrite = defined $tweaks && defined $tweaks->{overwrite_all} ? $tweaks->{useGenericAttrs}->{overwrite_all} : 'true';
|
|
my $url = qq{/api/slots?overwrite_all=$overwrite};
|
|
|
|
|
|
my @gdts = (qw(switch light media blind thermostat thermometer));
|
|
for my $gdt (@gdts) {
|
|
last if !$hash->{useGenericAttrs};
|
|
my @names = ();
|
|
my @groupnames = ();
|
|
my @devs = devspec2array("$hash->{devspec}");
|
|
for my $device (@devs) {
|
|
if (AttrVal($device, 'genericDeviceType', '') eq $gdt) {
|
|
push @names, split m{,}x, $hash->{helper}{devicemap}{devices}{$device}->{names};
|
|
push @groupnames, split m{,}x, $hash->{helper}{devicemap}{devices}{$device}->{groups};
|
|
}
|
|
}
|
|
@names = get_unique(\@names);
|
|
@names = ('') if !@names && $noEmpty;
|
|
$deviceData->{qq(${language}.${fhemId}.Device-${gdt})} = \@names if @names;
|
|
@groupnames = get_unique(\@groupnames);
|
|
@groupnames = ('') if !@groupnames && $noEmpty;
|
|
$deviceData->{qq(${language}.${fhemId}.Group-${gdt})} = \@groupnames if @groupnames;
|
|
}
|
|
|
|
my @allKeywords = uniq(@groups, @rooms, @devices);
|
|
|
|
$deviceData->{qq(${language}.${fhemId}.Device)} = \@devices if @devices;
|
|
$deviceData->{qq(${language}.${fhemId}.Room)} = \@rooms if @rooms;
|
|
$deviceData->{qq(${language}.${fhemId}.MediaChannels)} = \@channels if @channels;
|
|
$deviceData->{qq(${language}.${fhemId}.Color)} = \@colors if @colors;
|
|
$deviceData->{qq(${language}.${fhemId}.NumericType)} = \@types if @types;
|
|
$deviceData->{qq(${language}.${fhemId}.Group)} = \@groups if @groups;
|
|
$deviceData->{qq(${language}.${fhemId}.AllKeywords)} = \@allKeywords if @allKeywords;
|
|
|
|
$json = eval { toJSON($deviceData) };
|
|
|
|
Log3($hash->{NAME}, 5, "Updating Rhasspy Slots with data ($language): $json");
|
|
|
|
_sendToApi($hash, $url, $method, $json);
|
|
return;
|
|
}
|
|
|
|
# Send all devices, rooms, etc. to Rhasspy HTTP-API to update the slots
|
|
sub updateSingleSlot {
|
|
my $hash = shift // return;
|
|
my $slotname = shift // return;
|
|
my $slotdata = shift // return;
|
|
my $overwr = shift // q{true};
|
|
my $training = shift;
|
|
$overwr = q{false} if $overwr ne 'true';
|
|
my @data = split m{,}xms, $slotdata;
|
|
my $language = $hash->{LANGUAGE};
|
|
my $fhemId = $hash->{fhemId};
|
|
my $method = q{POST};
|
|
|
|
my $url = qq{/api/slots?overwrite_all=$overwr};
|
|
|
|
my $deviceData->{qq(${language}.${fhemId}.$slotname)} = \@data;
|
|
|
|
my $json = eval { toJSON($deviceData) };
|
|
|
|
Log3($hash->{NAME}, 5, "Updating Rhasspy single slot with data ($language): $json");
|
|
|
|
_sendToApi($hash, $url, $method, $json);
|
|
return trainRhasspy($hash) if $training;
|
|
|
|
return;
|
|
}
|
|
|
|
# Use the HTTP-API to instruct Rhasspy to re-train it's data
|
|
sub trainRhasspy {
|
|
my $hash = shift // return;
|
|
my $url = q{/api/train};
|
|
my $method = q{POST};
|
|
my $contenttype = q{application/json};
|
|
|
|
Log3($hash->{NAME}, 5, 'Starting training on Rhasspy');
|
|
return _sendToApi($hash, $url, $method, undef);
|
|
}
|
|
|
|
# Use the HTTP-API to fetch all available siteIds
|
|
sub fetchSiteIds {
|
|
my $hash = shift // return;
|
|
my $url = q{/api/profile?layers=profile};
|
|
my $method = q{GET};
|
|
|
|
Log3($hash->{NAME}, 5, 'fetchSiteIds called');
|
|
return _sendToApi($hash, $url, $method, undef);
|
|
}
|
|
|
|
=pod
|
|
# Check connection to HTTP-API
|
|
# Seems useless, because fetchSiteIds is called after DEF
|
|
sub RHASSPY_checkHttpApi {
|
|
my $hash = shift // return;
|
|
my $url = q{/api/unknown-words};
|
|
my $method = q{GET};
|
|
|
|
Log3($hash->{NAME}, 5, "check connection to Rhasspy HTTP-API");
|
|
return _sendToApi($hash, $url, $method, undef);
|
|
}
|
|
=cut
|
|
|
|
# Send request to HTTP-API of Rhasspy
|
|
sub _sendToApi {
|
|
my $hash = shift // return;
|
|
my $url = shift;
|
|
my $method = shift;
|
|
my $data = shift;
|
|
my $base = $hash->{baseUrl}; #AttrVal($hash->{NAME}, 'rhasspyMaster', undef) // return;
|
|
|
|
#Retrieve URL of Rhasspy-Master from attribute
|
|
$url = $base.$url;
|
|
|
|
my $apirequest = {
|
|
url => $url,
|
|
hash => $hash,
|
|
timeout => 120,
|
|
method => $method,
|
|
data => $data,
|
|
header => 'Content-Type: application/json',
|
|
callback => \&RHASSPY_ParseHttpResponse
|
|
};
|
|
|
|
HttpUtils_NonblockingGet($apirequest);
|
|
return;
|
|
}
|
|
|
|
# Parse the response of the request to the HTTP-API
|
|
sub RHASSPY_ParseHttpResponse {
|
|
my $param = shift // return;
|
|
my $err = shift;
|
|
my $data = shift;
|
|
my $hash = $param->{hash};
|
|
my $url = lc $param->{url};
|
|
|
|
my $name = $hash->{NAME};
|
|
my $base = $hash->{baseUrl}; #AttrVal($name, 'rhasspyMaster', undef) // return;
|
|
my $cp = $hash->{encoding} // q{UTF-8};
|
|
|
|
readingsBeginUpdate($hash);
|
|
|
|
if ($err) {
|
|
readingsBulkUpdate($hash, 'state', $err);
|
|
readingsEndUpdate($hash, 1);
|
|
Log3($hash->{NAME}, 1, "Connection to Rhasspy base failed: $err");
|
|
return;
|
|
}
|
|
|
|
my $urls = {
|
|
$base.'/api/train' => 'training',
|
|
$base.'/api/sentences' => 'updateSentences',
|
|
$base.'/api/slots?overwrite_all=true' => 'updateSlots'
|
|
};
|
|
|
|
if ( defined $urls->{$url} ) {
|
|
readingsBulkUpdate($hash, $urls->{$url}, $data);
|
|
if ( $urls->{$url} eq 'updateSlots' && $hash->{'.needTraining'} ) {
|
|
trainRhasspy($hash);
|
|
delete $hash->{'.needTraining'};
|
|
}
|
|
}
|
|
elsif ( $url =~ m{api/profile}ix ) {
|
|
my $ref;
|
|
if ( !eval { $ref = decode_json($data) ; 1 } ) {
|
|
readingsEndUpdate($hash, 1);
|
|
return Log3($hash->{NAME}, 1, "JSON decoding error: $@");
|
|
}
|
|
#my $ref = decode_json($data);
|
|
my $siteIds = encode($cp,$ref->{dialogue}{satellite_site_ids});
|
|
readingsBulkUpdate($hash, 'siteIds', $siteIds);
|
|
}
|
|
else {
|
|
Log3($name, 3, qq(error while requesting $param->{url} - $data));
|
|
}
|
|
readingsBulkUpdate($hash, 'state', 'online');
|
|
readingsEndUpdate($hash, 1);
|
|
return;
|
|
}
|
|
|
|
|
|
# Eingehender Custom-Intent
|
|
sub handleCustomIntent {
|
|
my $hash = shift // return;
|
|
my $intentName = shift;
|
|
my $data = shift;
|
|
|
|
if ( !defined $hash->{helper}{custom} || !defined $hash->{helper}{custom}{$intentName} ) {
|
|
Log3($hash->{NAME}, 2, "handleIntentCustomIntent called with invalid $intentName key");
|
|
return;
|
|
}
|
|
my $custom = $hash->{helper}{custom}{$intentName};
|
|
Log3($hash->{NAME}, 5, "handleCustomIntent called with $intentName key");
|
|
|
|
my ($intent, $response, $room);
|
|
|
|
if ( exists $data->{Device} ) {
|
|
$room = getRoomName($hash, $data);
|
|
$data->{Device} = getDeviceByName($hash, $room, $data->{Device}); #Beta-User: really...?
|
|
}
|
|
|
|
my $subName = $custom->{function};
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultError')) if !defined $subName;
|
|
|
|
my $params = $custom->{args};
|
|
my @rets = @{$params};
|
|
|
|
for (@rets) {
|
|
if ($_ eq 'NAME') {
|
|
$_ = qq{"$hash->{NAME}"};
|
|
} elsif ($_ eq 'DATA') {
|
|
my $json = toJSON($data);
|
|
$_ = qq{'$json'};
|
|
} elsif (defined $data->{$_}) {
|
|
$_ = qq{"$data->{$_}"};
|
|
} else {
|
|
$_ = "undef";
|
|
}
|
|
}
|
|
|
|
my $args = join q{,}, @rets;
|
|
my $cmd = qq{ $subName( $args ) };
|
|
Log3($hash->{NAME}, 5, "Calling sub: $cmd" );
|
|
my $error = AnalyzePerlCommand($hash, $cmd);
|
|
if ( ref $error eq 'ARRAY' ) {
|
|
$response = ${$error}[0] // getResponse($hash, 'DefaultConfirmation');
|
|
if ( ref ${$error}[0] eq 'HASH') {
|
|
my $timeout = ${$error}[1];
|
|
$timeout = defined $timeout && looks_like_number($timeout) ? $timeout : 20;
|
|
$hash->{'.toTrigger'} = ${$error}[1] if defined ${$error}[1];
|
|
return RHASSPY_Confirmation($hash, 2, $data, $timeout, ${$error}[0]);
|
|
}
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return ${$error}[1]; #comma separated list of devices to trigger
|
|
} elsif ( ref $error eq 'HASH' ) {
|
|
return RHASSPY_Confirmation($hash, 2, $data, 20, $error);
|
|
} else {
|
|
$response = $error; # if $error && $error !~ m{Please.define.*first}x;
|
|
}
|
|
|
|
$response = getResponse($hash, 'DefaultConfirmation') if !defined $response;
|
|
|
|
# Antwort senden
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
|
|
# Handle incoming "SetMute" intents
|
|
sub handleIntentSetMute {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $response;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentSetMute called");
|
|
|
|
if ( exists $data->{Value} && exists $data->{siteId} ) {
|
|
my $siteId = makeReadingName($data->{siteId});
|
|
readingsSingleUpdate($hash, "mute_$siteId", $data->{Value} eq 'on' ? 1 : 0, 1);
|
|
$response = getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
$response = $response // getResponse($hash, 'DefaultError');
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
# Handle custom Shortcuts
|
|
sub handleIntentShortcuts {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $cfdd = shift // 0;
|
|
|
|
my $shortcut = $hash->{helper}{shortcuts}{$data->{input}};
|
|
Log3($hash->{NAME}, 5, "handleIntentShortcuts called with $data->{input} key");
|
|
|
|
my $response;
|
|
if ( defined $hash->{helper}{shortcuts}{$data->{input}}{conf_timeout} && !$data->{Confirmation} ) {
|
|
my $timeout = $hash->{helper}{shortcuts}{$data->{input}}{conf_timeout};
|
|
$response = $hash->{helper}{shortcuts}{$data->{input}}{conf_req};return RHASSPY_Confirmation($hash, 2, $data, $timeout, $response);
|
|
}
|
|
$response = $shortcut->{response} // getResponse($hash, 'DefaultConfirmation');
|
|
my $ret;
|
|
my $device = $shortcut->{NAME};
|
|
my $cmd = $shortcut->{perl};
|
|
|
|
my $self = $hash->{NAME};
|
|
my $name = $shortcut->{NAME} // $self;
|
|
my %specials = (
|
|
'$DEVICE' => $name,
|
|
'$SELF' => $self,
|
|
'$NAME' => $name
|
|
);
|
|
|
|
if ( defined $cmd ) {
|
|
Log3($hash->{NAME}, 5, "Perl shortcut identified: $cmd, device name is $name");
|
|
|
|
$cmd = _replace($hash, $cmd, \%specials);
|
|
#execute Perl command
|
|
$cmd = qq({$cmd}) if ($cmd !~ m{\A\{.*\}\z}x);
|
|
|
|
$ret = analyzeAndRunCmd($hash, undef, $cmd, undef, $data->{siteId});
|
|
$device = $ret if $ret && $ret !~ m{Please.define.*first}x;
|
|
|
|
$response = $ret // _replace($hash, $response, \%specials);
|
|
} elsif ( defined $shortcut->{fhem} ) {
|
|
$cmd = $shortcut->{fhem} // return;
|
|
Log3($hash->{NAME}, 5, "FHEM shortcut identified: $cmd, device name is $name");
|
|
$cmd = _replace($hash, $cmd, \%specials);
|
|
$response = _replace($hash, $response, \%specials);
|
|
AnalyzeCommand($hash, $cmd);
|
|
}
|
|
$response = _ReplaceReadingsVal( $hash, $response );
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
# update Readings
|
|
#updateLastIntentReadings($hash, $topic,$data);
|
|
|
|
return $device;
|
|
}
|
|
|
|
# Handle incoming "SetOnOff" intents
|
|
sub handleIntentSetOnOff {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my ($value, $numericValue, $device, $room, $siteId, $mapping, $response);
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentSetOnOff called");
|
|
|
|
# Device AND Value must exist
|
|
if ( exists $data->{Device} && exists $data->{Value} ) {
|
|
$room = getRoomName($hash, $data);
|
|
$value = $data->{Value};
|
|
$value = $value eq $de_mappings->{on} ? 'on' : $value; #Beta-User: compability
|
|
$device = getDeviceByName($hash, $room, $data->{Device});
|
|
$mapping = getMapping($hash, $device, 'SetOnOff', undef, defined $hash->{helper}{devicemap});
|
|
|
|
# Mapping found?
|
|
if ( defined $device && defined $mapping ) {
|
|
my $cmdOn = $mapping->{cmdOn} // 'on';
|
|
my $cmdOff = $mapping->{cmdOff} // 'off';
|
|
my $cmd = $value eq 'on' ? $cmdOn : $cmdOff;
|
|
|
|
# execute Cmd
|
|
analyzeAndRunCmd($hash, $device, $cmd);
|
|
Log3($hash->{NAME}, 5, "Running command [$cmd] on device [$device]" );
|
|
|
|
# Define response
|
|
if ( defined $mapping->{response} ) {
|
|
$numericValue = $value eq 'on' ? 1 : 0;
|
|
$response = _getValue($hash, $device, $mapping->{response}, $numericValue, $room);
|
|
Log3($hash->{NAME}, 5, "Response is $response" );
|
|
}
|
|
else { $response = getResponse($hash, 'DefaultConfirmation'); }
|
|
}
|
|
}
|
|
# Send response
|
|
$response = $response // getResponse($hash, 'DefaultError');
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $device;
|
|
}
|
|
|
|
sub handleIntentSetOnOffGroup {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentSetOnOffGroup called");
|
|
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoValidData')) if !defined $data->{Value};
|
|
|
|
my $devices = getDevicesByGroup($hash, $data);
|
|
|
|
#see https://perlmaven.com/how-to-sort-a-hash-of-hashes-by-value for reference
|
|
my @devlist = sort {
|
|
$devices->{$a}{prio} <=> $devices->{$b}{prio}
|
|
or
|
|
$devices->{$a}{delay} <=> $devices->{$b}{delay}
|
|
} keys %{$devices};
|
|
|
|
Log3($hash, 5, 'sorted devices list is: ' . join q{ }, @devlist);
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound')) if !keys %{$devices};
|
|
|
|
my $delaysum = 0;
|
|
|
|
my $value = $data->{Value};
|
|
$value = $value eq $de_mappings->{on} ? 'on' : $value;
|
|
|
|
my $updatedList;
|
|
|
|
my $init_delay = 0;
|
|
my $needs_sorting = (@{$hash->{".asyncQueue"}});
|
|
|
|
for my $device (@devlist) {
|
|
my $mapping = getMapping($hash, $device, 'SetOnOff', undef, defined $hash->{helper}{devicemap});
|
|
|
|
# Mapping found?
|
|
next if !defined $mapping;
|
|
|
|
my $cmdOn = $mapping->{cmdOn} // 'on';
|
|
my $cmdOff = $mapping->{cmdOff} // 'off';
|
|
my $cmd = $value eq 'on' ? $cmdOn : $cmdOff;
|
|
|
|
# execute Cmd
|
|
if ( !$delaysum ) {
|
|
analyzeAndRunCmd($hash, $device, $cmd);
|
|
Log3($hash->{NAME}, 5, "Running command [$cmd] on device [$device]" );
|
|
$delaysum += $devices->{$device}->{delay};
|
|
$updatedList = $updatedList ? "$updatedList,$device" : $device;
|
|
} else {
|
|
my $hlabel = $devices->{$device}->{delay};
|
|
push @{$hash->{".asyncQueue"}}, {device => $device, cmd => $cmd, prio => $devices->{$device}->{prio}, delay => $hlabel};
|
|
InternalTimer(time+$delaysum,\&RHASSPY_asyncQueue,$hash,0) if !$init_delay;
|
|
$init_delay = 1;
|
|
}
|
|
}
|
|
|
|
_sortAsyncQueue($hash) if $init_delay && $needs_sorting;
|
|
|
|
# Send response
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultConfirmation'));
|
|
return $updatedList;
|
|
}
|
|
|
|
# Handle incomint GetOnOff intents
|
|
sub handleIntentGetOnOff {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $device;
|
|
my $response;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentGetOnOff called");
|
|
|
|
# Device AND Status must exist
|
|
if ( exists $data->{Device} && exists $data->{State} ) {
|
|
my $room = getRoomName($hash, $data);
|
|
$device = getDeviceByName($hash, $room, $data->{Device});
|
|
my $deviceName = $data->{Device};
|
|
my $mapping;
|
|
$mapping = getMapping($hash, $device, 'GetOnOff', undef, defined $hash->{helper}{devicemap}, 0) if defined $device;
|
|
my $status = $data->{State};
|
|
|
|
# Mapping found?
|
|
if ( defined $mapping ) {
|
|
# Device on or off?
|
|
my $value = _getOnOffState($hash, $device, $mapping);
|
|
|
|
# Define reponse
|
|
if ( defined $mapping->{response} ) {
|
|
$response = _getValue($hash, $device, $mapping->{response}, $value, $room);
|
|
}
|
|
else {
|
|
my $stateResponseType = $internal_mappings->{stateResponseType}->{$status} // $de_mappings->{stateResponseType}->{$status};
|
|
$response = $hash->{helper}{lng}->{stateResponses}{$stateResponseType}->{$value};
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
}
|
|
}
|
|
}
|
|
# Send response
|
|
$response = getResponse($hash, 'DefaultError') if !defined $response;
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $device;
|
|
}
|
|
|
|
|
|
sub isValidData {
|
|
my $data = shift // return 0;
|
|
my $validData = 0;
|
|
|
|
$validData = 1 if exists $data->{Device} && ( exists $data->{Value} || exists $data->{Change}) #);
|
|
|
|
# Mindestens Device und Change angegeben -> Valid (z.B. Radio lauter)
|
|
#|| exists $data->{Device} && exists $data->{Change}
|
|
# Nur Change für Lautstärke angegeben -> Valid (z.B. lauter)
|
|
#|| !exists $data->{Device} && defined $data->{Change}
|
|
# && defined $hash->{helper}{lng}->{regex}->{$data->{Change}}
|
|
|| !exists $data->{Device} && defined $data->{Change}
|
|
&& (defined $internal_mappings->{Change}->{$data->{Change}} ||defined $de_mappings->{ToEn}->{$data->{Change}})
|
|
#$data->{Change}= =~ m/^(lauter|leiser)$/i);
|
|
|
|
|
|
# Nur Type = Lautstärke und Value angegeben -> Valid (z.B. Lautstärke auf 10)
|
|
#||!exists $data->{Device} && defined $data->{Type} && exists $data->{Value} && $data->{Type} =~
|
|
#m{\A$hash->{helper}{lng}->{Change}->{regex}->{volume}\z}xim;
|
|
|| !exists $data->{Device} && defined $data->{Type} && exists $data->{Value} && ( $data->{Type} eq 'volume' || $data->{Type} eq 'Lautstärke' );
|
|
|
|
return $validData;
|
|
}
|
|
|
|
sub handleIntentSetNumericGroup {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
Log3($hash->{NAME}, 5, 'handleIntentSetNumericGroup called');
|
|
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoValidData')) if !exists $data->{Value} && !exists $data->{Change};
|
|
|
|
my $devices = getDevicesByGroup($hash, $data);
|
|
|
|
#see https://perlmaven.com/how-to-sort-a-hash-of-hashes-by-value for reference
|
|
my @devlist = sort {
|
|
$devices->{$a}{prio} <=> $devices->{$b}{prio}
|
|
or
|
|
$devices->{$a}{delay} <=> $devices->{$b}{delay}
|
|
} keys %{$devices};
|
|
|
|
Log3($hash, 5, 'sorted devices list is: ' . join q{ }, @devlist);
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound')) if !keys %{$devices};
|
|
|
|
my $delaysum = 0;
|
|
|
|
my $value = $data->{Value};
|
|
$value = $value eq $de_mappings->{on} ? 'on' : $value;
|
|
|
|
my $updatedList;
|
|
|
|
my $init_delay = 0;
|
|
my $needs_sorting = (@{$hash->{'.asyncQueue'}});
|
|
|
|
for my $device (@devlist) {
|
|
my $tempdata = $data;
|
|
$tempdata->{'.DevName'} = $device;
|
|
$tempdata->{'.inBulk'} = 1;
|
|
|
|
# execute Cmd
|
|
if ( !$delaysum ) {
|
|
handleIntentSetNumeric($hash, $tempdata);
|
|
Log3($hash->{NAME}, 5, "Running SetNumeric on device [$device]" );
|
|
$delaysum += $devices->{$device}->{delay};
|
|
$updatedList = $updatedList ? "$updatedList,$device" : $device;
|
|
} else {
|
|
my $hlabel = $devices->{$device}->{delay};
|
|
push @{$hash->{'.asyncQueue'}}, {device => $device, SetNumeric => $tempdata, prio => $devices->{$device}->{prio}, delay => $hlabel};
|
|
InternalTimer(time+$delaysum,\&RHASSPY_asyncQueue,$hash,0) if !$init_delay;
|
|
$init_delay = 1;
|
|
}
|
|
}
|
|
|
|
_sortAsyncQueue($hash) if $init_delay && $needs_sorting;
|
|
|
|
# Send response
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultConfirmation'));
|
|
return $updatedList;
|
|
}
|
|
|
|
# Eingehende "SetNumeric" Intents bearbeiten
|
|
sub handleIntentSetNumeric {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $device = $data->{'.DevName'};
|
|
#my $mapping;
|
|
my $response;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentSetNumeric called");
|
|
|
|
if ( !defined $device && !isValidData($data) ) {
|
|
return if defined $data->{'.inBulk'};
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoValidData'));
|
|
}
|
|
|
|
my $unit = $data->{Unit};
|
|
my $change = $data->{Change};
|
|
my $type = $data->{Type}
|
|
# Type not defined? try to derive from Type (en and de)
|
|
// $internal_mappings->{Change}->{$change}->{Type}
|
|
// $internal_mappings->{Change}->{$de_mappings->{ToEn}->{$change}}->{Type};
|
|
my $value = $data->{Value};
|
|
my $room = getRoomName($hash, $data);
|
|
|
|
|
|
# Gerät über Name suchen, oder falls über Lautstärke ohne Device getriggert wurde das ActiveMediaDevice suchen
|
|
if ( !defined $device && exists $data->{Device} ) {
|
|
$device = getDeviceByName($hash, $room, $data->{Device});
|
|
} elsif ( defined $type && ( $type eq 'volume' || $type eq 'Lautstärke' ) ) {
|
|
$device =
|
|
getActiveDeviceForIntentAndType($hash, $room, 'SetNumeric', $type)
|
|
// return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoActiveMediaDevice'));
|
|
}
|
|
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound')) if !defined $device;
|
|
|
|
my $mapping = getMapping($hash, $device, 'SetNumeric', $type, defined $hash->{helper}{devicemap}, 0);
|
|
|
|
if ( !defined $mapping ) {
|
|
if ( defined $data->{'.inBulk'} ) {
|
|
#Beta-User: long forms to later add options to check upper/lower limits for pure on/off devices
|
|
return;
|
|
} else {
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoMappingFound'));
|
|
}
|
|
}
|
|
|
|
# Mapping and device found -> execute command
|
|
my $cmd = $mapping->{cmd} // return defined $data->{'.inBulk'} ? undef : respond($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoMappingFound'));
|
|
my $part = $mapping->{part};
|
|
my $minVal = $mapping->{minVal};
|
|
my $maxVal = $mapping->{maxVal};
|
|
|
|
$minVal = 0 if defined $minVal && !looks_like_number($minVal);
|
|
$maxVal = 100 if defined $maxVal && !looks_like_number($maxVal);
|
|
my $checkMinMax = defined $minVal && defined $maxVal ? 1 : 0;
|
|
|
|
my $diff = $value // $mapping->{step} // 10;
|
|
|
|
#my $up = (defined($change) && ($change =~ m/^(höher|heller|lauter|wärmer)$/)) ? 1 : 0;
|
|
my $up = $change;
|
|
$up = $internal_mappings->{Change}->{$change}->{up}
|
|
// $internal_mappings->{Change}->{$de_mappings->{ToEn}->{$change}}->{up}
|
|
// ($change =~ m{\A$internal_mappings->{regex}->{upward}\z}xi || $change =~ m{\A$de_mappings->{regex}->{upward}\z}xi ) ? 1
|
|
: 0;
|
|
|
|
my $forcePercent = ( defined $mapping->{map} && lc $mapping->{map} eq 'percent' ) ? 1 : 0;
|
|
|
|
# Alten Wert bestimmen
|
|
my $oldVal = _getValue($hash, $device, $mapping->{currentVal});
|
|
|
|
if (defined $part) {
|
|
my @tokens = split m{\s+}x, $oldVal;
|
|
$oldVal = $tokens[$part] if @tokens >= $part;
|
|
}
|
|
|
|
# Neuen Wert bestimmen
|
|
my $newVal;
|
|
my $ispct = defined $unit && ( $unit eq 'percent' || $unit eq $de_mappings->{percent} ) ? 1 : 0;
|
|
|
|
if ( !defined $change ) {
|
|
# Direkter Stellwert ("Stelle Lampe auf 50")
|
|
#if ($unit ne 'Prozent' && defined $value && !defined $change && !$forcePercent) {
|
|
if ( !defined $value ) {
|
|
#do nothing...
|
|
} elsif ( !$ispct && !$forcePercent ) {
|
|
$newVal = $value;
|
|
} elsif ( ( $ispct || $forcePercent ) && $checkMinMax ) {
|
|
# Direkter Stellwert als Prozent ("Stelle Lampe auf 50 Prozent", oder "Stelle Lampe auf 50" bei forcePercent)
|
|
#elsif (defined $value && ( defined $unit && $unit eq 'Prozent' || $forcePercent ) && !defined $change && defined $minVal && defined $maxVal) {
|
|
|
|
# Wert von Prozent in Raw-Wert umrechnen
|
|
$newVal = $value;
|
|
#$newVal = 0 if ($newVal < 0);
|
|
#$newVal = 100 if ($newVal > 100);
|
|
$newVal = round((($newVal * (($maxVal - $minVal) / 100)) + $minVal), 0);
|
|
}
|
|
} else { # defined $change
|
|
# Stellwert um Wert x ändern ("Mache Lampe um 20 heller" oder "Mache Lampe heller")
|
|
#elsif ((!defined $unit || $unit ne 'Prozent') && defined $change && !$forcePercent) {
|
|
if ( ( !defined $unit || !$ispct ) && !$forcePercent ) {
|
|
$newVal = ($up) ? $oldVal + $diff : $oldVal - $diff;
|
|
}
|
|
# Stellwert um Prozent x ändern ("Mache Lampe um 20 Prozent heller" oder "Mache Lampe um 20 heller" bei forcePercent oder "Mache Lampe heller" bei forcePercent)
|
|
#elsif (($unit eq 'Prozent' || $forcePercent) && defined($change) && defined $minVal && defined $maxVal) {
|
|
elsif ( ( $ispct || $forcePercent ) && $checkMinMax ) {
|
|
#$maxVal = 100 if !looks_like_number($maxVal); #Beta-User: Workaround, should be fixed in mapping (tbd)
|
|
#my $diffRaw = round((($diff * (($maxVal - $minVal) / 100)) + $minVal), 0);
|
|
my $diffRaw = round(($diff * ($maxVal - $minVal) / 100), 0);
|
|
$newVal = ($up) ? $oldVal + $diffRaw : $oldVal - $diffRaw;
|
|
$newVal = max( $minVal, min( $maxVal, $newVal ) );
|
|
}
|
|
}
|
|
|
|
if ( !defined $newVal ) {
|
|
return defined $data->{'.inBulk'} ? undef : respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoNewValDerived'));
|
|
}
|
|
|
|
# limit to min/max (if set)
|
|
$newVal = max( $minVal, $newVal ) if defined $minVal;
|
|
$newVal = min( $maxVal, $newVal ) if defined $maxVal;
|
|
|
|
# execute Cmd
|
|
analyzeAndRunCmd($hash, $device, $cmd, $newVal);
|
|
|
|
#venetian blind special
|
|
my $specials = $hash->{helper}{devicemap}{devices}{$device}{venetian_specials};
|
|
if ( defined $specials ) {
|
|
my $vencmd = $specials->{setter} // $cmd;
|
|
my $vendev = $specials->{device} // $device;
|
|
analyzeAndRunCmd($hash, $vendev, defined $specials->{CustomCommand} ? $specials->{CustomCommand} :$vencmd , $newVal) if $device ne $vendev || $cmd ne $vencmd;
|
|
}
|
|
|
|
# get response
|
|
defined $mapping->{response}
|
|
? $response = _getValue($hash, $device, $mapping->{response}, $newVal, $room)
|
|
: $response = getResponse($hash, 'DefaultConfirmation');
|
|
|
|
# send response
|
|
$response = getResponse($hash, 'DefaultError') if !defined $response;
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response) if !defined $data->{'.inBulk'};
|
|
return $device;
|
|
}
|
|
|
|
|
|
# Eingehende "GetNumeric" Intents bearbeiten
|
|
sub handleIntentGetNumeric {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $value;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentGetNumeric called");
|
|
|
|
# Mindestens Type oder Device muss existieren
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultError')) if !exists $data->{Type} && !exists $data->{Device};
|
|
|
|
my $type = $data->{Type};
|
|
my $subType = $data->{subType} // $type;
|
|
my $room = getRoomName($hash, $data);
|
|
|
|
# Get suitable device
|
|
my $device = exists $data->{Device}
|
|
? getDeviceByName($hash, $room, $data->{Device})
|
|
: getDeviceByIntentAndType($hash, $room, 'GetNumeric', $type)
|
|
// return respond($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound'));
|
|
|
|
my $mapping = getMapping($hash, $device, 'GetNumeric', { type => $type, subType => $subType }, defined $hash->{helper}{devicemap}, 0)
|
|
// return respond($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoMappingFound'));
|
|
|
|
# Mapping found
|
|
my $part = $mapping->{part};
|
|
my $minVal = $mapping->{minVal};
|
|
my $maxVal = $mapping->{maxVal};
|
|
my $mappingType = $mapping->{type};
|
|
my $forcePercent = defined $mapping->{map} && lc($mapping->{map}) eq 'percent' && defined $minVal && defined $maxVal ? 1 : 0;
|
|
|
|
# Get value for response
|
|
$value = _getValue($hash, $device, $mapping->{currentVal});
|
|
if ( defined $part ) {
|
|
my @tokens = split m{\s+}x, $value;
|
|
$value = $tokens[$part] if @tokens >= $part;
|
|
}
|
|
$value = round( ($value * ($maxVal - $minVal) / 100 + $minVal), 0) if $forcePercent;
|
|
|
|
my $isNumber = looks_like_number($value);
|
|
# replace dot by comma if needed
|
|
$value =~ s{\.}{\,}gx if $hash->{helper}{lng}->{commaconversion};
|
|
|
|
my $location = $data->{Device};
|
|
if ( !defined $location ) {
|
|
my $rooms = $hash->{helper}{devicemap}{devices}{$device}->{rooms};
|
|
$location = $data->{Room} if defined $rooms && $rooms =~ m{\b$data->{Room}\b}ix;
|
|
|
|
#Beta-User: this might be the place to implement the "no device in room" branch
|
|
($location, my $nn) = split m{,}x, $rooms if !defined $location;
|
|
}
|
|
my $deviceName = $hash->{helper}{devicemap}{devices}{$device}->{alias} // $device;
|
|
|
|
# Antwort falls Custom Response definiert ist
|
|
if ( defined $mapping->{response} ) {
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, _getValue($hash, $device, $mapping->{response}, $value, $location));
|
|
}
|
|
my $responses = $hash->{helper}{lng}->{responses}->{Change};
|
|
|
|
# Antwort falls mappingType oder type matched
|
|
my $response =
|
|
$responses->{$mappingType}
|
|
// $responses->{$de_mappings->{ToEn}->{$mappingType}}
|
|
// $responses->{$type}
|
|
// $responses->{$de_mappings->{ToEn}->{$type}};
|
|
$response = $response->{$isNumber} if ref $response eq 'HASH';
|
|
#Log3($hash->{NAME}, 3, "#2378: resp is $response, mT is $mappingType");
|
|
|
|
# Antwort falls mappingType auf regex (en bzw. de) matched
|
|
if (!defined $response && (
|
|
$mappingType=~ m{\A$internal_mappings->{regex}->{setTarget}\z}xim
|
|
|| $mappingType=~ m{\A$de_mappings->{regex}->{setTarget}\z}xim)) {
|
|
$response = $responses->{setTarget};
|
|
#Log3($hash->{NAME}, 3, "#2384: resp now is $response");
|
|
}
|
|
if (!defined $response) {
|
|
#or not and at least know the type...?
|
|
$response = defined $mappingType
|
|
? $responses->{knownType}
|
|
: $responses->{unknownType};
|
|
}
|
|
|
|
# Variablen ersetzen?
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
# Antwort senden
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
|
|
# Handle incoming "GetState" intents
|
|
sub handleIntentGetState {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $device = $data->{Device} // return;
|
|
my $response;
|
|
|
|
Log3($hash->{NAME}, 5, 'handleIntentGetState called');
|
|
|
|
# Mindestens Device muss existieren
|
|
if (exists $data->{Device}) {
|
|
my $room = getRoomName($hash, $data);
|
|
$device = getDeviceByName($hash, $room, $device);
|
|
my $mapping = getMapping($hash, $device, 'GetState', undef, defined $hash->{helper}{devicemap}, 0);
|
|
|
|
if ( defined $mapping->{response} ) {
|
|
$response = _getValue($hash, $device, $mapping->{response}, undef, $room);
|
|
$response = _ReplaceReadingsVal($hash, $mapping->{response}) if !$response; #Beta-User: case: plain Text with [device:reading]
|
|
}
|
|
}
|
|
# Antwort senden
|
|
$response = getResponse($hash, 'DefaultError') if !defined $response;
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
|
|
# Handle incomint "MediaControls" intents
|
|
sub handleIntentMediaControls {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $command, my $device, my $room;
|
|
my $mapping;
|
|
my $response = getResponse($hash, 'DefaultError');
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentMediaControls called");
|
|
|
|
# At least one command has to be received
|
|
if (exists $data->{Command}) {
|
|
$room = getRoomName($hash, $data);
|
|
$command = $data->{Command};
|
|
|
|
# Search for matching device
|
|
if (exists $data->{Device}) {
|
|
$device = getDeviceByName($hash, $room, $data->{Device});
|
|
} else {
|
|
$device = getActiveDeviceForIntentAndType($hash, $room, 'MediaControls', undef);
|
|
$response = getResponse($hash, 'NoActiveMediaDevice') if !defined $device;
|
|
}
|
|
|
|
$mapping = getMapping($hash, $device, 'MediaControls', undef, defined $hash->{helper}{devicemap}, 0);
|
|
|
|
if (defined $device && defined $mapping) {
|
|
my $cmd = $mapping->{$command};
|
|
|
|
#Beta-User: backwards compability check; might be removed later...
|
|
if (!defined $cmd) {
|
|
my $Media = {
|
|
play => 'cmdPlay', pause => 'cmdPause',
|
|
stop => 'cmdStop', vor => 'cmdFwd', next => 'cmdFwd',
|
|
'zurück' => 'cmdBack', previous => 'cmdBack'
|
|
};
|
|
$cmd = $mapping->{ $Media->{$command} };
|
|
Log3($hash->{NAME}, 4, "MediaControls with outdated mapping $command called. Please change to avoid future problems...");
|
|
}
|
|
|
|
else {
|
|
# Execute Cmd
|
|
analyzeAndRunCmd($hash, $device, $cmd);
|
|
|
|
# Define voice response
|
|
$response = defined $mapping->{response} ?
|
|
_getValue($hash, $device, $mapping->{response}, $command, $room)
|
|
: getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
}
|
|
}
|
|
# Send voice response
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $device;
|
|
}
|
|
|
|
|
|
# Handle incoming "GetTime" intents
|
|
sub handleIntentGetTime {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
Log3($hash->{NAME}, 5, "handleIntentGetTime called");
|
|
|
|
(my $sec,my $min,my $hour,my $mday,my $mon,my $year,my $wday,my $yday,my $isdst) = localtime;
|
|
my $response = $hash->{helper}{lng}->{responses}->{timeRequest};
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
Log3($hash->{NAME}, 5, "Response: $response");
|
|
|
|
# Send voice reponse
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
|
|
# Handle incoming "GetWeekday" intents
|
|
sub handleIntentGetWeekday {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentGetWeekday called");
|
|
|
|
my $weekDay = strftime( '%A', localtime );
|
|
my $response = $hash->{helper}{lng}->{responses}->{weekdayRequest};
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
|
|
Log3($hash->{NAME}, 5, "Response: $response");
|
|
|
|
# Send voice reponse
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
}
|
|
|
|
|
|
# Eingehende "MediaChannels" Intents bearbeiten
|
|
sub handleIntentMediaChannels {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $channel; my $device; my $room;
|
|
my $cmd;
|
|
my $response; # = getResponse($hash, 'DefaultError');
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentMediaChannels called");
|
|
|
|
# Mindestens Channel muss übergeben worden sein
|
|
if ( exists $data->{Channel} ) {
|
|
$room = getRoomName($hash, $data);
|
|
$channel = $data->{Channel};
|
|
|
|
# Passendes Gerät suchen
|
|
if ( exists $data->{Device} ) {
|
|
$device = getDeviceByName($hash, $room, $data->{Device});
|
|
} else {
|
|
$device = getDeviceByMediaChannel($hash, $room, $channel);
|
|
}
|
|
|
|
if (defined $hash->{helper}{devicemap}) {
|
|
$cmd = $hash->{helper}{devicemap}{devices}{$device}{Channels}{$channel};
|
|
}
|
|
else {
|
|
$cmd = getKeyValFromAttr($hash, $device, 'rhasspyChannels', $channel, undef);
|
|
}
|
|
#$cmd = (split m{=}x, $cmd, 2)[1];
|
|
|
|
if ( defined $device && defined $cmd ) {
|
|
$response = getResponse($hash, 'DefaultConfirmation');
|
|
# Cmd ausführen
|
|
analyzeAndRunCmd($hash, $device, $cmd);
|
|
}
|
|
}
|
|
|
|
# Antwort senden
|
|
$response = getResponse($hash, 'NoMediaChannelFound') if !defined $response;
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $device;
|
|
}
|
|
|
|
|
|
# Handle incoming "SetColor" intents
|
|
sub handleIntentSetColor {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
my $inBulk = $data->{'.inBulk'} // 0;
|
|
my $device = $data->{'.DevName'};
|
|
|
|
Log3($hash->{NAME}, 5, "handleIntentSetColor called");
|
|
my $response;
|
|
|
|
# At least Device AND Color have to be received
|
|
if ( !exists $data->{Color} && !exists $data->{Rgb} &&!exists $data->{Saturation} && !exists $data->{Colortemp} && !exists $data->{Hue} || !exists $data->{Device} && !defined $device) {
|
|
return if $inBulk;
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoValidData')) ;
|
|
}
|
|
|
|
#if (exists $data->{Color} && exists $data->{Device}) {
|
|
my $room = getRoomName($hash, $data);
|
|
my $color = $data->{Color} // q{};
|
|
|
|
# Search for matching device and command
|
|
$device = getDeviceByName($hash, $room, $data->{Device}) if !defined $device;
|
|
my $cmd = getKeyValFromAttr($hash, $device, 'rhasspyColors', $color, undef);
|
|
my $cmd2;
|
|
if (defined $hash->{helper}{devicemap}{devices}{$device}{color_specials}
|
|
&& defined $hash->{helper}{devicemap}{devices}{$device}{color_specials}->{CommandMap}) {
|
|
$cmd2 = $hash->{helper}{devicemap}{devices}{$device}{color_specials}->{CommandMap}->{$color};
|
|
}
|
|
|
|
return if $inBulk && !defined $device;
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound')) if !defined $device;
|
|
|
|
if ( defined $cmd || defined $cmd2 ) {
|
|
$response = getResponse($hash, 'DefaultConfirmation');
|
|
# Execute Cmd
|
|
analyzeAndRunCmd( $hash, $device, defined $cmd ? $cmd : $cmd2 );
|
|
} else {
|
|
$response = _runSetColorCmd($hash, $device, $data, $inBulk);
|
|
}
|
|
# Send voice response
|
|
$response = getResponse($hash, 'DefaultError') if !defined $response;
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response) if !$inBulk;
|
|
return $device;
|
|
}
|
|
|
|
sub _runSetColorCmd {
|
|
my $hash = shift // return;
|
|
my $device = shift // return;
|
|
my $data = shift // return;
|
|
my $inBulk = shift // 0;
|
|
|
|
my $color = $data->{Color};
|
|
|
|
my $mapping = $hash->{helper}{devicemap}{devices}{$device}{intents}{SetColorParms} // return $inBulk ?undef : respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoMappingFound'));
|
|
|
|
my $error;
|
|
|
|
#shortcuts: hue, sat or CT are directly addressed and possible commands
|
|
my $keywords = {hue => 'Hue', sat => 'Saturation', ct => 'Colortemp'};
|
|
for (keys %{$keywords}) {
|
|
my $kw = $keywords->{$_};
|
|
|
|
my $forceRgb = $hash->{helper}{devicemap}{devices}{$device}{color_specials}->{forceHue2rgb} // 0;
|
|
next if defined $kw && $kw eq 'Hue' && $forceRgb == 1;
|
|
|
|
my $specialmapping = $hash->{helper}{devicemap}{devices}{$device}{color_specials}{$kw};
|
|
if (defined $data->{$kw} && defined $specialmapping && defined $specialmapping->{$data->{$kw}}) {
|
|
my $cmd = $specialmapping->{$data->{$kw}};
|
|
$error = AnalyzeCommand($hash, "set $device $cmd");
|
|
return if $inBulk;
|
|
Log3($hash->{NAME}, 5, "Setting $device to $cmd");
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $error) if $error;
|
|
return getResponse($hash, 'DefaultConfirmation');
|
|
} elsif ( defined $data->{$kw} && defined $mapping->{$_} ) {
|
|
my $value = round( ($mapping->{$_}->{maxVal} - $mapping->{$_}->{minVal}) * $data->{$kw} / ($kw eq 'Hue' ? 360 : 100) , 0);
|
|
$value = min(max($mapping->{$_}->{minVal}, $value), $mapping->{$_}->{maxVal});
|
|
$error = AnalyzeCommand($hash, "set $device $mapping->{$_}->{cmd} $value");
|
|
return if $inBulk;
|
|
Log3($hash->{NAME}, 5, "Setting color to $value");
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $error) if $error;
|
|
return getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
}
|
|
|
|
#shortcut: Rgb field is used or color is in HEX value and rgb is a possible command
|
|
if ( ( defined $data->{Rgb} || defined $color && $color =~ m{\A[[:xdigit:]]\z}x ) && defined $mapping->{rgb} ) {
|
|
$color = $data->{Rgb} if defined $data->{Rgb};
|
|
$error = AnalyzeCommand($hash, "set $device $mapping->{rgb}->{cmd} $color");
|
|
return if $inBulk;
|
|
Log3($hash->{NAME}, 5, "Setting rgb-color to $color");
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $error) if $error;
|
|
return getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
|
|
#only matches, if there's no native hue command
|
|
if ( defined $data->{Hue} && defined $mapping->{rgb} ) {
|
|
my $angle = int($data->{Hue} / 24)*15;
|
|
my $angle2rgb = {
|
|
# from https://en.wikipedia.org/wiki/Hue#24_hues_of_HSL/HSV
|
|
# hue angle color code luminance
|
|
0 => {rgb => 'FF0000' , brightness => '30'},
|
|
15=> { rgb => 'FF4000', brightness => '45' },
|
|
30=> { rgb => 'FF8000', brightness => '59' },
|
|
45=> { rgb => 'FFBF00', brightness => '74' },
|
|
60=> { rgb => 'FFFF00', brightness => '89' },
|
|
75=> { rgb => 'BFFF00', brightness => '81' },
|
|
90=> { rgb => '80FF00', brightness => '74' },
|
|
105=> { rgb => '40FF00', brightness => '66' },
|
|
120=> { rgb => '00FF00', brightness => '59' },
|
|
135=> { rgb => '00FF40', brightness => '62' },
|
|
150=> { rgb => '00FF80', brightness => '64' },
|
|
165=> { rgb => '00FFBF', brightness => '67' },
|
|
180=> { rgb => '00FFFF', brightness => '70' },
|
|
195=> { rgb => '00BFFF', brightness => '55' },
|
|
210=> { rgb => '0080FF', brightness => '41' },
|
|
225=> { rgb => '0040FF', brightness => '26' },
|
|
240=> { rgb => '0000FF', brightness => '11' },
|
|
255=> { rgb => '4000FF', brightness => '19' },
|
|
270=> { rgb => '8000FF', brightness => '26' },
|
|
285=> { rgb => 'BF00FF', brightness => '34' },
|
|
300=> { rgb => 'FF00FF', brightness => '41' },
|
|
315=> { rgb => 'FF00BF', brightness => '38' },
|
|
330=> { rgb => 'FF0080', brightness => '36' },
|
|
345=> { rgb => 'FF0040', brightness => '33' }
|
|
};
|
|
my $rgb = $angle2rgb->{$angle}->{rgb};
|
|
return "mapping problem in Hue2rgb" if !defined $rgb;
|
|
my $rgbcmd = $mapping->{rgb}->{cmd};
|
|
$rgb = lc $rgb if $rgbcmd eq 'hex';
|
|
$error = AnalyzeCommand($hash, "set $device $rgbcmd $rgb");
|
|
return if $inBulk;
|
|
Log3($hash->{NAME}, 5, "Setting rgb-color to $rgb using hue");
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $error) if $error;
|
|
return getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
|
|
if ( defined $data->{Colortemp} && defined $mapping->{rgb} && looks_like_number($data->{Colortemp}) ) {
|
|
|
|
my $ct = $data->{Colortemp}*50 + 2000; #FHEMWIKI indicates typical range from 2000 to 6500
|
|
my ($r, $g, $b) = _ct2rgb($ct);
|
|
my $rgb = uc sprintf( "%2.2X%2.2X%2.2X", $r, $g, $b );
|
|
|
|
return "mapping problem in _ct2rgb" if !defined $rgb;
|
|
$error = AnalyzeCommand($hash, "set $device $mapping->{rgb}->{cmd} $rgb");
|
|
return if $inBulk;
|
|
Log3($hash->{NAME}, 5, "Setting color-temperature to $ct");
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $error) if $error;
|
|
return getResponse($hash, 'DefaultConfirmation');
|
|
}
|
|
|
|
return if $inBulk;
|
|
return getResponse($hash, 'NoMappingFound');
|
|
}
|
|
|
|
#clone from Color.pm
|
|
sub _ct2rgb {
|
|
my $ct = shift // return;
|
|
|
|
# calculation from http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code
|
|
|
|
# kelvin -> mired
|
|
$ct = 1000000/$ct if( $ct > 1000 );
|
|
|
|
# adjusted by 1000K
|
|
my $temp = 10000/$ct + 10;
|
|
|
|
my $r = 255;
|
|
$r = 329.698727446 * ($temp - 60) ** -0.1332047592 if $temp > 66;
|
|
$r = max( 0, min ( $r , 255 ) );
|
|
|
|
my $g = $temp <= 66 ?
|
|
99.4708025861 * log($temp) - 161.1195681661
|
|
: 288.1221695283 * ($temp - 60) ** -0.0755148492;
|
|
$g = max( 0, min ( $g , 255 ) );
|
|
|
|
my $bl = $temp <= 19 ? 0 : 255;
|
|
$bl = 138.5177312231 * log($temp-10) - 305.0447927307 if $temp < 66;
|
|
$bl = max( 0, min ( $b , 255 ) );
|
|
|
|
return( $r, $g, $bl );
|
|
}
|
|
|
|
sub handleIntentSetColorGroup {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
Log3($hash->{NAME}, 5, 'handleIntentSetColorGroup called');
|
|
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoValidData')) if !exists $data->{Color} && !exists $data->{Rgb} &&!exists $data->{Saturation} && !exists $data->{Colortemp} && !exists $data->{Hue};
|
|
|
|
my $devices = getDevicesByGroup($hash, $data);
|
|
|
|
#see https://perlmaven.com/how-to-sort-a-hash-of-hashes-by-value for reference
|
|
my @devlist = sort {
|
|
$devices->{$a}{prio} <=> $devices->{$b}{prio}
|
|
or
|
|
$devices->{$a}{delay} <=> $devices->{$b}{delay}
|
|
} keys %{$devices};
|
|
|
|
Log3($hash, 5, 'sorted devices list is: ' . join q{ }, @devlist);
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'NoDeviceFound')) if !keys %{$devices};
|
|
|
|
my $delaysum = 0;
|
|
my $updatedList;
|
|
my $init_delay = 0;
|
|
my $needs_sorting = (@{$hash->{'.asyncQueue'}});
|
|
|
|
for my $device (@devlist) {
|
|
my $tempdata = $data;
|
|
$tempdata->{'.DevName'} = $device;
|
|
$tempdata->{'.inBulk'} = 1;
|
|
|
|
# execute Cmd
|
|
if ( !$delaysum ) {
|
|
handleIntentSetColor($hash, $data);
|
|
Log3($hash->{NAME}, 5, "Running SetColor on device [$device]" );
|
|
$delaysum += $devices->{$device}->{delay};
|
|
$updatedList = $updatedList ? "$updatedList,$device" : $device;
|
|
} else {
|
|
my $hlabel = $devices->{$device}->{delay};
|
|
push @{$hash->{'.asyncQueue'}}, {device => $device, SetColor => $tempdata, prio => $devices->{$device}->{prio}, delay => $hlabel};
|
|
InternalTimer(time+$delaysum,\&RHASSPY_asyncQueue,$hash,0) if !$init_delay;
|
|
$init_delay = 1;
|
|
}
|
|
}
|
|
|
|
_sortAsyncQueue($hash) if $init_delay && $needs_sorting;
|
|
|
|
# Send response
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultConfirmation'));
|
|
return $updatedList;
|
|
}
|
|
|
|
|
|
|
|
# Handle incoming SetTimer intents
|
|
sub handleIntentSetTimer {
|
|
my $hash = shift;
|
|
my $data = shift // return;
|
|
my $siteId = $data->{siteId} // return;
|
|
my $name = $hash->{NAME};
|
|
|
|
Log3($name, 5, 'handleIntentSetTimer called');
|
|
|
|
return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $hash->{helper}{lng}->{responses}->{duration_not_understood})
|
|
if !defined $data->{Hourabs} && !defined $data->{Hour} && !defined $data->{Min} && !defined $data->{Sec} && !defined $data->{CancelTimer};
|
|
|
|
my $room = getRoomName($hash, $data);
|
|
|
|
my $hour = 0;
|
|
my $value = time;
|
|
my $now = $value;
|
|
my @time = localtime($now);
|
|
if ( defined $data->{Hourabs} ) {
|
|
$hour = $data->{Hourabs};
|
|
$value = $value - ($time[2] * HOURSECONDS) - ($time[1] * MINUTESECONDS) - $time[0]; #last midnight
|
|
}
|
|
elsif ($data->{Hour}) {
|
|
$hour = $data->{Hour};
|
|
}
|
|
$value += HOURSECONDS * $hour;
|
|
$value += MINUTESECONDS * $data->{Min} if $data->{Min};
|
|
$value += $data->{Sec} if $data->{Sec};
|
|
|
|
my $tomorrow = 0;
|
|
if ( $value < $now ) {
|
|
$tomorrow = 1;
|
|
$value += +DAYSECONDS;
|
|
}
|
|
|
|
my $siteIds = ReadingsVal( $name, 'siteIds',0);
|
|
fetchSiteIds($hash) if !$siteIds;
|
|
|
|
my $timerRoom = $siteId;
|
|
|
|
my $responseEnd = $hash->{helper}{lng}->{responses}->{timerEnd}->{1};
|
|
|
|
if ($siteIds =~ m{\b$room\b}ix) {
|
|
$timerRoom = $room if $siteIds =~ m{\b$room\b}ix;
|
|
$responseEnd = $hash->{helper}{lng}->{responses}->{timerEnd}->{0};
|
|
}
|
|
|
|
my $roomReading = "timer_".makeReadingName($room);
|
|
my $label = $data->{Label} // q{};
|
|
$roomReading .= "_$label" if $label ne '';
|
|
|
|
my $response;
|
|
if (defined $data->{CancelTimer}) {
|
|
CommandDelete($hash, $roomReading);
|
|
#readingsSingleUpdate( $hash,$roomReading, 0, 1 );
|
|
readingsDelete($hash, $roomReading);
|
|
Log3($name, 5, "deleted timer: $roomReading");
|
|
$response = getResponse($hash, 'timerCancellation');
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $name;
|
|
}
|
|
|
|
if( $value && $timerRoom ) {
|
|
my $seconds = $value - $now;
|
|
my $diff = $seconds;
|
|
my $attime = strftime( '%H', gmtime $diff );
|
|
$attime += 24 if $tomorrow;
|
|
$attime .= strftime( ':%M:%S', gmtime $diff );
|
|
my $readingTime = strftime( '%H:%M:%S', localtime (time + $seconds));
|
|
|
|
$responseEnd =~ s{(\$\w+)}{$1}eegx;
|
|
|
|
my $soundoption = $hash->{helper}{tweaks}{timerSounds}->{$label} // $hash->{helper}{tweaks}{timerSounds}->{default};
|
|
#my $timerTrigger = $hash->{helper}{tweaks}->{timerTrigger};
|
|
#my $addtrigger = defined $timerTrigger && ( $timerTrigger eq 'default' || $timerTrigger =~ m{\b$label\b}x ) ?
|
|
#my $addtrigger = defined $timerTrigger && $label ne '' && $timerTrigger =~ m{\bdefault|$label\b}x ?
|
|
my $addtrigger = qq{; trigger $name timerEnd $siteId $room};
|
|
$addtrigger .= " $label" if defined $label;
|
|
# : q{};
|
|
|
|
if ( !defined $soundoption ) {
|
|
CommandDefMod($hash, "-temporary $roomReading at +$attime set $name speak siteId=\"$timerRoom\" text=\"$responseEnd\";deletereading $name ${roomReading}$addtrigger");
|
|
} else {
|
|
$soundoption =~ m{((?<repeats>[0-9]*)[:]){0,1}((?<duration>[0-9.]*)[:]){0,1}(?<file>(.+))}x;
|
|
my $file = $+{file} // Log3($hash->{NAME}, 2, "no WAV file for $label provided, check attribute rhasspyTweaks (item timerSounds)!") && return respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse($hash, 'DefaultError'));
|
|
my $repeats = $+{repeats} // 5;
|
|
my $duration = $+{duration} // 15;
|
|
CommandDefMod($hash, "-temporary $roomReading at +$attime set $name play siteId=\"$timerRoom\" path=\"$file\" repeats=$repeats wait=$duration id=${roomReading}$addtrigger");
|
|
}
|
|
|
|
#readingsSingleUpdate($hash, $roomReading, 1, 1);
|
|
readingsSingleUpdate($hash, $roomReading, $readingTime, 1);
|
|
|
|
#Log3($name, 5, "Created timer: $roomReading at +$attime");
|
|
Log3($name, 5, "Created timer: $roomReading at $readingTime");
|
|
|
|
my ($range, $minutes, $hours, $minutetext);
|
|
my @timerlimits = $hash->{helper}->{tweaks}->{timerLimits} // (91, 9*MINUTESECONDS, HOURSECONDS, 1.5*HOURSECONDS, HOURSECONDS );
|
|
@time = localtime($value);
|
|
if ( $seconds < $timerlimits[0] && ( !defined $data->{Hourabs} || defined $data->{Hourabs} && $seconds < $timerlimits[4] ) ) {
|
|
$range = 0;
|
|
} elsif ( $seconds < $timerlimits[2] && ( !defined $data->{Hourabs} || defined $data->{Hourabs} && $seconds < $timerlimits[4] ) ) {
|
|
$minutes = int ($seconds/MINUTESECONDS);
|
|
$range = $seconds < $timerlimits[1] ? 1 : 2;
|
|
$seconds = $seconds % MINUTESECONDS;
|
|
$range = 2 if !$seconds;
|
|
$minutetext = $hash->{helper}{lng}->{units}->{unitMinutes}->{$minutes > 1 ? 0 : 1};
|
|
$minutetext = qq{$minutes $minutetext} if $minutes > 1;
|
|
} elsif ( $seconds < $timerlimits[3] && ( !defined $data->{Hourabs} || defined $data->{Hourabs} && $seconds < $timerlimits[4] ) ) {
|
|
$hours = int ($seconds/HOURSECONDS);
|
|
$seconds = $seconds % HOURSECONDS;
|
|
$minutes = int ($seconds/MINUTESECONDS);
|
|
$range = 3;
|
|
$minutetext = $minutes ? $hash->{helper}{lng}->{units}->{unitMinutes}->{$minutes > 1 ? 0 : 1} : q{};
|
|
$minutetext = qq{$minutes $minutetext} if $minutes > 1;
|
|
} else {
|
|
$hours = $time[2];
|
|
$minutes = $time[1];
|
|
$range = 4 + $tomorrow;
|
|
}
|
|
$response = $hash->{helper}{lng}->{responses}->{timerSet}->{$range};
|
|
$response =~ s{(\$\w+)}{$1}eegx;
|
|
}
|
|
|
|
$response = getResponse($hash, 'DefaultError') if !defined $response;
|
|
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
return $name;
|
|
}
|
|
|
|
sub handleIntentConfirmAction {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
|
|
Log3($hash->{NAME}, 5, 'handleIntentConfirmAction called');
|
|
|
|
#cancellation case
|
|
return RHASSPY_Confirmation($hash, 1, $data) if $data->{Mode} ne 'OK';
|
|
|
|
#confirmed case
|
|
my $data_old = $hash->{helper}{'.delayed'};
|
|
|
|
return respond( $hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, getResponse( $hash, 'DefaultConfirmationNoOutstanding' ) ) if ! defined $data_old;
|
|
delete $hash->{helper}{'.delayed'};
|
|
|
|
$data_old->{siteId} = $data->{siteId};
|
|
$data_old->{sessionId} = $data->{sessionId};
|
|
$data_old->{requestType} = $data->{requestType};
|
|
$data_old->{Confirmation} = 1;
|
|
|
|
my $intent = $data_old->{intent};
|
|
my $device = $hash->{NAME};
|
|
|
|
# Passenden Intent-Handler aufrufen
|
|
if (ref $dispatchFns->{$intent} eq 'CODE') {
|
|
$device = $dispatchFns->{$intent}->($hash, $data_old);
|
|
}
|
|
|
|
return $device;
|
|
}
|
|
|
|
sub handleIntentReSpeak {
|
|
my $hash = shift // return;
|
|
my $data = shift // return;
|
|
my $name = $hash->{NAME};
|
|
|
|
my $response = ReadingsVal($name,'voiceResponse',$hash->{helper}{lng}->{responses}->{reSpeak_failed});
|
|
|
|
Log3($hash->{NAME}, 5, 'handleIntentReSpeak called');
|
|
|
|
respond ($hash, $data->{requestType}, $data->{sessionId}, $data->{siteId}, $response);
|
|
|
|
return $name;
|
|
}
|
|
|
|
sub setPlayWav {
|
|
my $hash = shift //return;
|
|
my $cmd = shift;
|
|
|
|
Log3($hash->{NAME}, 5, 'action playWav called');
|
|
|
|
return 'playWav needs siteId and path to file as parameters!' if !defined $cmd->{siteId} || !defined $cmd->{path};
|
|
|
|
my $siteId = $cmd->{siteId};
|
|
my $filename = $cmd->{path};
|
|
my $repeats = $cmd->{repeats};
|
|
my $encoding = q{:raw :bytes};
|
|
my $handle = undef;
|
|
my $topic = "hermes/audioServer/$siteId/playBytes/999";
|
|
|
|
Log3($hash->{NAME}, 3, "Playing file $filename on $siteId");
|
|
|
|
if (-e $filename) {
|
|
open $handle, "< $encoding", $filename || carp "$0: can't open $filename for reading: $!"; ##no critic qw(RequireBriefOpen)
|
|
while ( read $handle, my $file_contents, 1000000 ) {
|
|
IOWrite($hash, 'publish', qq{$topic $file_contents});
|
|
}
|
|
close $handle;
|
|
}
|
|
|
|
return if !$repeats;
|
|
my $name = $hash->{NAME};
|
|
my $wait = $cmd->{wait} // 15;
|
|
my $id = $cmd->{id};
|
|
|
|
$repeats--;
|
|
my $attime = strftime( '%H:%M:%S', gmtime $wait );
|
|
return InternalTimer(time, sub (){CommandDefMod($hash, "-temporary $id at +$attime set $name play siteId=\"$siteId\" path=\"$filename\" repeats=$repeats wait=$wait id=$id")}, $hash ) if $repeats;
|
|
return InternalTimer(time, sub (){CommandDefMod($hash, "-temporary $id at +$attime set $name play siteId=\"$siteId\" path=\"$filename\" repeats=$repeats wait=$wait")}, $hash ) if !$id;
|
|
return InternalTimer(time, sub (){CommandDefMod($hash, "-temporary $id at +$attime set $name play siteId=\"$siteId\" path=\"$filename\" repeats=$repeats wait=$wait; deletereading $name $id")}, $hash );
|
|
}
|
|
|
|
# Set volume on specific siteId
|
|
sub setVolume {
|
|
my $hash = shift // return;
|
|
my $cmd = shift;
|
|
|
|
return 'setVolume needs siteId and volume as parameters!' if !defined $cmd->{siteId} || !defined $cmd->{volume};
|
|
|
|
my $sendData = {
|
|
id => '0',
|
|
sessionId => '0'
|
|
};
|
|
|
|
Log3($hash->{NAME}, 5, 'setVolume called');
|
|
|
|
$sendData->{siteId} = $cmd->{siteId};
|
|
$sendData->{volume} = 0 + $cmd->{volume};
|
|
|
|
my $json = toJSON($sendData);
|
|
return IOWrite($hash, 'publish', qq{rhasspy/audioServer/setVolume $json});
|
|
|
|
}
|
|
|
|
|
|
# Abgespeckte Kopie von ReplaceSetMagic aus fhem.pl
|
|
sub _ReplaceReadingsVal {
|
|
my $hash = shift;
|
|
my $arr = shift // return;
|
|
|
|
my $to_analyze = $arr;
|
|
|
|
my $readingsVal = sub ($$$$$) {
|
|
my $all = shift;
|
|
my $t = shift;
|
|
my $d = shift;
|
|
my $n = shift;
|
|
my $s = shift;
|
|
my $val;
|
|
my $dhash = $defs{$d};
|
|
return $all if !$dhash;
|
|
|
|
if(!$t || $t eq 'r:') {
|
|
my $r = $dhash->{READINGS};
|
|
if($s && ($s eq ':t' || $s eq ':sec')) {
|
|
return $all if !$r || !$r->{$n};
|
|
$val = $r->{$n}{TIME};
|
|
$val = int(gettimeofday()) - time_str2num($val) if $s eq ':sec';
|
|
return $val;
|
|
}
|
|
$val = $r->{$n}{VAL} if $r && $r->{$n};
|
|
}
|
|
$val = $dhash->{$n} if !defined $val && (!$t || $t eq 'i:');
|
|
$val = $attr{$d}{$n} if !defined $val && (!$t || $t eq 'a:') && $attr{$d};
|
|
return $all if !defined $val;
|
|
|
|
if($s && $s =~ m{:d|:r|:i}x && $val =~ m{(-?\d+(\.\d+)?)}x) {
|
|
$val = $1;
|
|
$val = int($val) if $s eq ':i';
|
|
$val = round($val, defined $1 ? $1 : 1) if $s =~ m{\A:r(\d)?}x;
|
|
}
|
|
return $val;
|
|
};
|
|
|
|
$to_analyze =~s{(\[([ari]:)?([a-zA-Z\d._]+):([a-zA-Z\d._\/-]+)(:(t|sec|i|d|r|r\d))?\])}{$readingsVal->($1,$2,$3,$4,$5)}egx;
|
|
return $to_analyze;
|
|
}
|
|
|
|
sub _getDataFile {
|
|
my $hash = shift // return;
|
|
my $filename = shift;
|
|
my $name = $hash->{NAME};
|
|
my $lang = $hash->{LANGUAGE};
|
|
$filename = $filename // AttrVal($name,'languageFile',undef);
|
|
my @t = localtime gettimeofday();
|
|
$filename = ResolveDateWildcards($filename, @t);
|
|
$hash->{CONFIGFILE} = $filename; # for configDB migration
|
|
return $filename;
|
|
}
|
|
|
|
sub _readLanguageFromFile {
|
|
my $hash = shift // return;
|
|
my $cfg = shift // return 0, toJSON($languagevars);
|
|
|
|
my $name = $hash->{NAME};
|
|
my $filename = _getDataFile($hash, $cfg);
|
|
Log3($name, 5, "trying to read language from $filename");
|
|
my ($ret, @content) = FileRead($filename);
|
|
if ($ret) {
|
|
Log3($name, 1, "$name failed to read languageFile $filename!") ;
|
|
return $ret, undef;
|
|
}
|
|
my @cleaned = grep { $_ !~ m{\A\s*[#]}x } @content;
|
|
|
|
return 0, join q{ }, @cleaned;
|
|
}
|
|
|
|
|
|
1;
|
|
|
|
__END__
|
|
|
|
=pod
|
|
|
|
=begin ToDo
|
|
|
|
# Farben:
|
|
Warum die Abfrage nach rgb? <code>if ( defined $data->{Colortemp} && defined $mapping->{rgb} && looks_like_number($data->{Colortemp}) ) {</code>
|
|
Gibt auch Lampen, die können nur ct
|
|
|
|
# PERL WARNING: Useless use of private variable in void context at ./FHEM/10_RHASSPY.pm line 1638, <$fh> line 310.
|
|
# [DEVICE:READING] Einträge ersetzen
|
|
$returnVal = ($hash, $cmd);
|
|
|
|
# Custom Intents
|
|
- Bei Verwendung des Dialouges wenn man keine Antwort spricht, bricht Rhasspy ab. Die voice response "Tut mir leid, da hat etwas zu lange gedauert" wird
|
|
also gar nicht ausgegeben und:
|
|
PERL WARNING: Use of uninitialized value $cmd in pattern match (m//) at fhem.pl line 5868.
|
|
|
|
# "rhasspySpecials" bzw. rhasspyTweaks als weitere Attribute
|
|
Denkbare Verwendung:
|
|
- siteId2room für mobile Geräte (Denkbare Anwendungsfälle: Auswertung BT-RSSI per Perl, aktives Setzen über ein Reading? Oder einen intent? (tweak)
|
|
- Ansteuerung von Lamellenpositionen (auch an anderem Device?) (special) (erledigt?)
|
|
- Bestätigungs-Mapping (special)
|
|
|
|
# Sonstiges, siehe insbes. https://forum.fhem.de/index.php/topic,119447.msg1148832.html#msg1148832
|
|
- kein "match in room" bei GetNumeric
|
|
- "kind" und wie man es füllen könnte (mehr Dialoge)
|
|
- Bestätigungsdialoge - weitere Anwendungsfelder
|
|
- gDT: mehr und bessere mappings?
|
|
- Farbe und Farbtemperatur (fast fertig?)
|
|
- Hat man in einem Raum einen Satelliten aber kein Device mit der siteId/Raum, kann man den Satelliten bei z.B. dem Timer nicht ansprechen, weil der Raum nicht in den Slots ist.
|
|
Irgendwie müssen wir die neue siteId in den Slot Rooms bringen
|
|
|
|
=end ToDo
|
|
|
|
=begin ToClarify
|
|
|
|
#defaultRoom (JensS):
|
|
- überhaupt erforderlich?
|
|
- Schreibweise: RHASSPY ist raus, Rhasspy scheint der überkommene Raumname für die devspec zu sein => ist erst mal weiter beides drin
|
|
|
|
# GetTimer implementieren?
|
|
https://forum.fhem.de/index.php/topic,113180.msg1130139.html#msg1130139
|
|
|
|
# Wetterdurchsage
|
|
Ist möglich. Dazu hatte ich einen rudimentären Intent in diesem Thread erstellt. Müsste halt nur erweitert werden.
|
|
https://forum.fhem.de/index.php/topic,113180.msg1130754.html#msg1130754
|
|
|
|
=end ToClarify
|
|
|
|
=encoding utf8
|
|
=item device
|
|
=item summary Control FHEM with Rhasspy voice assistant
|
|
=item summary_DE Steuerung von FHEM mittels Rhasspy Sprach-Assistent
|
|
=begin html
|
|
|
|
<a id="RHASSPY"></a>
|
|
<h3>RHASSPY</h3>
|
|
<p>This module receives, processes and executes voice commands coming from <a href="https://rhasspy.readthedocs.io/en/latest/">Rhasspy voice assistent</a>.</p>
|
|
|
|
<a id="RHASSPY-define"></a>
|
|
<h4>Define</h4>
|
|
<p><code>define <name> RHASSPY <baseUrl> <devspec> <defaultRoom> <language> <fhemId> <prefix> <useGenericAttrs> <encoding></code></p>
|
|
<p><b>All parameters in define are optional, but changing them later might lead to confusing results!</b></p>
|
|
<p><a id="RHASSPY-parseParams"></a><b>General Remark:</b> RHASSPY uses <a href="https://wiki.fhem.de/wiki/DevelopmentModuleAPI#parseParams"><b>parseParams</b></a> at quite a lot places, not only in define, but also to parse attribute values.<br>
|
|
So all parameters in define should be provided in the <i>key=value</i> form. In other places you may have to start e.g. a single line in an attribute with <code>option:key="value xy shall be z"</code> or <code>identifier:yourCode={fhem("set device off")} anotherOption=blabla</code> form.</p>
|
|
|
|
<ul>
|
|
<li><b>baseUrl</b>: http-address of the Rhasspy service web-interface. Optional. Default is <code>baseUrl=http://127.0.0.1:12101</code>.<br>Make sure, this is set to correct values (ip and port)</li>
|
|
<li><b>devspec</b>: A description of devices that should be controlled by Rhasspy. Optional. Default is <code>devspec=room=Rhasspy</code>, see <a href="#devspec"> as a reference</a>, how to e.g. use a comma-separated list of devices or combinations like <code>devspec=room=livingroom,room=bathroom,bedroomlamp</code>.</li>
|
|
<li><b>defaultRoom</b>: Default room name. Used to speak commands without a room name (e.g. "turn lights on" to turn on the lights in the "default room"). Optional. Default is <code>defaultRoom=default</code>.</li>
|
|
<li><b>language</b>: Makes part of the topic tree, RHASSPY is listening to. Should (but needs not to) point to the language voice commands shall be spoken with. Default is derived from global, which defaults to <code>language=en</code></li>
|
|
<li><b>encoding</b>: May be helpfull in case you experience problems in conversion between RHASSPY (module) and Rhasspy (service). Example: <code>encoding=cp-1252</code></li>
|
|
<li><b>fhemId</b>: May be used to distinguishe between different instances of RHASSPY on the MQTT side. Also makes part of the topic tree the corresponding RHASSPY is listening to.<br>
|
|
Might be usefull, if you have several instances of FHEM running, and may - in later versions - be a criteria to distinguish between different users (e.g. to only allow a subset of commands and/or rooms to be addressed).</li>
|
|
<li><b>prefix</b>: May be used to distinguishe between different instances of RHASSPY on the FHEM-internal side.<br>
|
|
Might be usefull, if you have several instances of RHASSPY in one FHEM running and want e.g. to use different identifier for groups and rooms (e.g. a different language).</li>
|
|
<li><b>useGenericAttrs</b>: By default, RHASSPY only uses it's own attributes (see list below) to identifiy options for the subordinated devices you want to control. Activating this with <code>useGenericAttrs=1</code> adds <code>genericDeviceType</code> to the global attribute list and activates RHASSPY's feature to estimate appropriate settings - similar to rhasspyMapping. In later versions <code>homebridgeMapping</code> may also be on the list.</li>
|
|
</ul>
|
|
|
|
<p>RHASSPY needs a <a href="#MQTT2_CLIENT">MQTT2_CLIENT</a> device connected to the same MQTT-Server as the voice assistant (Rhasspy) service.</p>
|
|
<p><b>Example for defining an MQTT2_CLIENT device and the Rhasspy device in FHEM:</b></p>
|
|
<p><code>defmod rhasspyMQTT2 MQTT2_CLIENT 192.168.1.122:12183<br>
|
|
attr rhasspyMQTT2 clientOrder RHASSPY MQTT_GENERIC_BRIDGE MQTT2_DEVICE<br>
|
|
attr rhasspyMQTT2 subscriptions hermes/intent/+ hermes/dialogueManager/sessionStarted hermes/dialogueManager/sessionEnded</code></p>
|
|
<p><code>define Rhasspy RHASSPY devspec=room=Rhasspy defaultRoom=Livingroom language=en</code></p>
|
|
|
|
<p><a id="RHASSPY-list"></a><b>Note:</b> RHASSPY consolidates a lot of data from different sources. The <b>final data structure RHASSPY uses</b> at runtime can be viewed using the <a href="#list">list command</a>. It's highly recommended to have a close look at this data structure, especially when starting with RHASSPY or in case something doesn't work as expected!<br>
|
|
When changing something relevant within FHEM for either the data structure in</p>
|
|
<ul>
|
|
<li><b>RHASSPY</b> (this form is used when reffering to module or the FHEM device) or for </li>
|
|
<li><b>Rhasspy</b> (this form is used when reffering to the remote service), </li>
|
|
</ul>
|
|
<p>these changes must be get to known to RHASSPY and (often, but not allways) to Rhasspy. See the different versions provided by the <a href="#RHASSPY-set-update">update command</a>.</p>
|
|
|
|
<p><b>Additionals remarks on MQTT2-IOs:</b></p>
|
|
<p>Using a separate MQTT server (and not the internal MQTT2_SERVER) is highly recommended, as the Rhasspy scripts also use the MQTT protocol for internal (sound!) data transfers. Best way is to either use MQTT2_CLIENT (see below) or bridge only the relevant topics from mosquitto to MQTT2_SERVER (see e.g. <a href="http://www.steves-internet-guide.com/mosquitto-bridge-configuration/">http://www.steves-internet-guide.com/mosquitto-bridge-configuration</a> for the principles). When using MQTT2_CLIENT, it's necessary to set <code>clientOrder</code> to include RHASSPY (as most likely, it's the only module listening to the CLIENT). It could be just set to <code>attr <m2client> clientOrder RHASSPY</code></p>
|
|
<p>Furthermore, you are highly encouraged to restrict subscriptions only to the relevant topics:</p>
|
|
<p><code>attr <m2client> subscriptions setByTheProgram</code></p>
|
|
<p>In case you are using the MQTT server also for other purposes than Rhasspy, you have to set <code>subscriptions</code> manually to at least include the following topics additionally to the other subscriptions desired for other purposes.</p>
|
|
<p><code>hermes/intent/+<br>
|
|
hermes/dialogueManager/sessionStarted<br>
|
|
hermes/dialogueManager/sessionEnded</code></p>
|
|
|
|
<a id="RHASSPY-set"></a>
|
|
<h4>Set</h4>
|
|
<ul>
|
|
<li>
|
|
<a id="RHASSPY-set-update"></a><b>update</b>
|
|
<p>Choose between one of the following:</p>
|
|
<ul>
|
|
<li><b>devicemap</b><br>
|
|
When having finished the configuration work to RHASSPY and the subordinated devices, issuing a devicemap-update is mandatory, to get the RHASSPY data structure updated, inform Rhasspy on changes that may have occured (update slots) and initiate a training on updated slot values etc., see <a href="#RHASSPY-list">remarks on data structure above</a>.
|
|
</li>
|
|
<li><b>devicemap_only</b><br>
|
|
This may be helpfull to make an intermediate check, whether attribute changes have found their way to the data structure. This will neither update slots nor initiate any training towards Rhasspy.
|
|
</li>
|
|
<li><b>slots</b><br>
|
|
This may be helpfull after checks on the FHEM side to send all data to Rhasspy and initiate training.
|
|
</li>
|
|
<li><b>slots_no_training</b><br>
|
|
This may be helpfull to make checks, whether all data is sent to Rhasspy. This will not initiate any training.
|
|
</li>
|
|
<li><b>language</b><br>
|
|
Reinitialization of language file.<br>
|
|
Be sure to execute this command after changing something within in the language configuration file!<br>
|
|
</li>
|
|
<li><b>all</b><br>
|
|
Surprise: means language file and full update to RHASSPY and Rhasspy including training.
|
|
</li>
|
|
</ul>
|
|
<p>Example: <code>set <rhasspyDevice> update language</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-play"></a><b>play</b>
|
|
<p>Send WAV file to Rhasspy.<br>
|
|
<i>siteId</i> and <i>path</i> are required!<br>
|
|
You may optionally add a number of repeats and a wait time in seconds between repeats. <i>wait</i> defaults to 15, if only <i>repeats</i> is given.</p>
|
|
<p>Examples:<br>
|
|
<code>set <rhasspyDevice> play siteId="default" path="/opt/fhem/test.wav"</code><br>
|
|
<code>set <rhasspyDevice> play siteId="default" path="./test.wav" repeats=3 wait=20</code>
|
|
</p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-speak"></a><b>speak</b>
|
|
<p>Voice output over TTS.<br>
|
|
Both arguments (siteId and text) are required!</p>
|
|
<p>Example:<br>
|
|
<code>set <rhasspyDevice> speak siteId="default" text="This is a test"</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-textCommand"></a><b>textCommand</b>
|
|
<p>Send a text command to Rhasspy.</p>
|
|
<p>Example:<br>
|
|
<code>set <rhasspyDevice> textCommand turn the light on</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-fetchSiteIds"></a><b>fetchSiteIds</b>
|
|
<p>Send a request to Rhasspy to send all siteId's. This by default is done once, so in case you add more satellites to your system, this may help to get RHASSPY updated.</p>
|
|
<p>Example:<br>
|
|
<code>set <rhasspyDevice> fetchSiteIds</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-trainRhasspy"></a><b>trainRhasspy</b>
|
|
<p>Sends a train-command to the HTTP-API of the Rhasspy master<br>
|
|
Might be removed in the future versions in favor of the update features</p>
|
|
<p>Example:<br>
|
|
<code>set <rhasspyDevice> trainRhasspy</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-volume"></a><b>volume</b>
|
|
<p>Sets volume of given siteId between 0 and 1 (float)<br>
|
|
Both arguments (siteId and volume) are required!</p>
|
|
<p>Example:<br>
|
|
<code>set <rhasspyDevice> siteId="default" volume="0.5"</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-set-customSlot"></a><b>customSlot</b>
|
|
<p>Creates a new - or overwrites an existing slot - in Rhasspy<br>
|
|
Provide slotname, slotdata and (optional) info, if existing data shall be overwritten and training shall be initialized immediately afterwards.<br>
|
|
First two arguments are required, third and fourth are optional.<br>
|
|
<i>overwrite</i> defaults to <i>true</i>, setting any other value than <i>true</i> will keep existing Rhasspy slot data.</p>
|
|
<p>Examples:<br>
|
|
<code>set <rhasspyDevice> customSlot mySlot a,b,c overwrite training </code><br>
|
|
<code>set <rhasspyDevice> customSlot slotname=mySlot slotdata=a,b,c overwrite=false</code></p>
|
|
</li>
|
|
</ul>
|
|
|
|
<a id="RHASSPY-attr"></a>
|
|
<h4>Attributes</h4>
|
|
<p>Note: To get RHASSPY working properly, you have to configure attributes at RHASSPY itself and the subordinated devices as well.</p>
|
|
|
|
<a id="RHASSPY-attr-device"></a>
|
|
<p><b>RHASSPY itself</b> supports the following attributes:</p>
|
|
<ul>
|
|
<li>
|
|
<a id="RHASSPY-attr-languageFile"></a><b>languageFile</b><br>
|
|
<p>Path to the language-config file. If this attribute isn't set, a default set of english responses is used for voice responses.<br>
|
|
The file itself must contain a JSON-encoded keyword-value structure (partly with sub-structures) following the given structure for the mentioned english defaults. As a reference, there's one available in german, or just make a dump of the English structure with e.g. (replace RHASSPY by your device's name): <code>{toJSON($defs{RHASSPY}->{helper}{lng})}</code>, edit the result e.g. using https://jsoneditoronline.org and place this in your own languageFile version. There might be some variables to be used - these should also work in your sentences.<br>
|
|
languageFile also allows combining e.g. a default set of german sentences with some few own modifications by using "defaults" subtree for the defaults and "user" subtree for your modified versions. This feature might be helpful in case the base language structure has to be changed in the future.</p>
|
|
<p>Example (placed in the same dir fhem.pl is located):</p>
|
|
<p><code>attr <rhasspyDevice> languageFile ./rhasspy-de.cfg</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-attr-response"></a><b>response</b>
|
|
<p><b>Not recommended. Use the language-file instead.</b></p>
|
|
<p>Optionally define alternative default answers. Available keywords are <code>DefaultError</code>, <code>NoActiveMediaDevice</code> and <code>DefaultConfirmation</code>.</p>
|
|
<p>Example:</p>
|
|
<p><code>DefaultError=
|
|
DefaultConfirmation=Klaro, mach ich</code></p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyIntents"></a><b>rhasspyIntents</b>
|
|
<p>Defines custom intents. See <a href="https://github.com/Thyraz/Snips-Fhem#f%C3%BCr-fortgeschrittene-eigene-custom-intents-erstellen-und-in-fhem-darauf-reagieren" hreflang="de">Custom Intent erstellen</a>.<br>
|
|
One intent per line.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr <rhasspyDevice> rhasspyIntents SetCustomIntentsTest=SetCustomIntentsTest(siteId,Type)</code></p>
|
|
<p>together with the following myUtils-Code should get a short impression of the possibilities:</p>
|
|
<p><code>sub SetCustomIntentsTest {<br>
|
|
my $room = shift;<br>
|
|
my $type = shift;<br>
|
|
Log3('rhasspy',3 , "RHASSPY: Room $room, Type $type");<br>
|
|
return "RHASSPY: Room $room, Type $type";<br>
|
|
}</code></p>
|
|
<p>The following arguments can be handed over:</p>
|
|
<ul>
|
|
<li>NAME => name of the RHASSPY device addressed, </li>
|
|
<li>DATA => entire JSON-$data (as parsed internally), encoded in JSON</li>
|
|
<li>siteId, Device etc. => any element out of the JSON-$data.</li>
|
|
</ul>
|
|
<p>If a simple text is returned, this will be considered as response.<br>
|
|
For more advanced use of this feature, you may return an array. First element of the array will be interpreted as comma-separated list of devices that may have been modified (otherwise, these devices will not cast any events! See also the "d" parameter in <a href="#RHASSPY-attr-rhasspyShortcuts"><i>rhasspyShortcuts</i></a>). The second element is interpreted as response and may either be simple text or HASH-type data. This will keep the dialogue-session open to allow interactive data exchange with <i>Rhasspy</i>. An open dialogue will be closed after some time, default is 20 seconds, you may alternatively hand over other numeric values as third element of the array.</p>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyShortcuts"></a><b>rhasspyShortcuts</b>
|
|
<p>Define custom sentences without editing Rhasspys sentences.ini<br>
|
|
The shortcuts are uploaded to Rhasspy when using the updateSlots set-command.<br>
|
|
One shortcut per line, syntax is either a simple and an extended version.</p>
|
|
<p>Examples:</p>
|
|
<p><code>mute on=set amplifier2 mute on<br>
|
|
lamp off={fhem("set lampe1 off")}<br>
|
|
i="you are so exciting" f="set $NAME speak siteId='livingroom' text='Thanks a lot, you are even more exciting!'"<br>
|
|
i="mute off" p={fhem ("set $NAME mute off")} n=amplifier2 c="Please confirm!"<br>
|
|
i="i am hungry" f="set Stove on" d="Stove" c="would you like roast pork"</code></p>
|
|
<p>Abbreviations explanation:</p>
|
|
<ul>
|
|
<li><b>i</b> => intent<br>
|
|
Lines starting with "i:" will be interpreted as extended version, so if you want to use that syntax style, starting with "i:" is mandatory.</li>
|
|
<li><b>f</b> => FHEM command<br>
|
|
Syntax as usual in FHEMWEB command field.</li>
|
|
<li><b>p</b> => Perl command<br>
|
|
Syntax as usual in FHEMWEB command field, enclosed in {}; this has priority to "f=".</li>
|
|
<li><b>d</b> => device name(s, comma separated) that shall be handed over to fhem.pl as updated. Needed for triggering further actions and longpoll! If not set, the return value of the called function will be used. </li>
|
|
<li><b>r</b> => Response to be send to the caller. If not set, the return value of the called function will be used.<br>
|
|
Response sentence will be parsed to do "set magic"-like replacements, so also a line like <code>i="what's the time for sunrise" r="at [Astro:SunRise] o'clock"</code> is valid.<br>
|
|
You may ask for confirmation as well using the following (optional) shorts:
|
|
<ul>
|
|
<li><b>c</b> => either numeric or text. If numeric: Timeout to wait for automatic cancellation. If text: response to send to ask for confirmation.</li>
|
|
<li><b>ct</b> => numeric value for timeout in seconds, default: 15.</li>
|
|
</ul></li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyTweaks"></a><b>rhasspyTweaks</b>
|
|
<p>Currently sets additional settings for timers and slot-updates to Rhasspy. May contain further custom settings in future versions like siteId2room info or code links, allowed commands, confirmation requests etc.</p>
|
|
<ul>
|
|
<li><b>timerLimits</b>
|
|
<p>Used to determine when the timer should response with e.g. "set to 30 minutes" or with "set to 10:30"</p>
|
|
<p><code>timerLimits=90,300,3000,2*HOURSECONDS,50</code></p>
|
|
<p>Five values have to be set, corresponding with the limits to <i>timerSet</i> responses. so above example will lead to seconds response for less then 90 seconds, minute+seconds response for less than 300 seconds etc.. Last value is the limit in seconds, if timer is set in time of day format.</p>
|
|
</li>
|
|
<li><b>timerSounds</b>
|
|
<p>Per default the timer responds with a voice command if it has elapsed. If you want to use a wav-file instead, you can set this here.</p>
|
|
<p><code>timerSounds= default=./yourfile1.wav eggs=3:20:./yourfile2.wav potatoes=5:./yourfile3.wav</code></p>
|
|
<p>Above keys are some examples and need to match the "Label"-tags for the timer provided by the Rhasspy-sentences.<br>
|
|
<i>default</i> is optional. If set, this file will be used for all labeled timer without match to other keywords.<br>
|
|
The two numbers are optional. The first one sets the number of repeats, the second is the waiting time between the repeats.<br>
|
|
<i>repeats</i> defaults to 5, <i>wait</i> to 15<br>
|
|
If only one number is set, this will be taken as <i>repeats</i>.</p>
|
|
</li>
|
|
<li><b>updateSlots</b>
|
|
<p>Changes aspects on slot generation and updates.</p>
|
|
<p><code>noEmptySlots=1</code></p>
|
|
<p>By default, RHASSPY will generate an additional slot for each of the genericDeviceType it recognizes, regardless, if there's any devices marked to belong to this type. If set to <i>1</i>, no empty slots will be generated.</p>
|
|
<p><code>overwrite_all=false</code></p>
|
|
<p>By default, RHASSPY will overwrite all generated slots. Setting this to <i>false</i> will change this.</p>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
|
|
<li>
|
|
<a id="RHASSPY-attr-forceNext"></a><b>forceNEXT</b>
|
|
<p>If set to 1, RHASSPY will forward incoming messages also to further MQTT2-IO-client modules like MQTT2_DEVICE, even if the topic matches to one of it's own subscriptions. By default, these messages will not be forwarded for better compability with autocreate feature on MQTT2_DEVICE. See also <a href="#MQTT2_CLIENTclientOrder">clientOrder attribute in MQTT2 IO-type commandrefs</a>; setting this in one instance of RHASSPY might affect others, too.</p>
|
|
</li>
|
|
</ul>
|
|
|
|
<p> </p>
|
|
<a id="RHASSPY-attr-subdevice"></a>
|
|
<p><b>For the subordinated devices</b>, a list of the possible attributes is automatically extended by several further entries</p>
|
|
<p>The names of these attributes all start with the <i>prefix</i> previously defined in RHASSPY - except for <a href="#RHASSPY-genericDeviceType">genericDeviceType</a> (gDT).<br>
|
|
These attributes are used to configure the actual mapping to the intents and the content sent by Rhasspy.</p>
|
|
<p>Note: As the analyses of the gDT is intented to lead to fast configuration progress, it's highly recommended to use this as a starting point. All other RHASSPY-specific attributes will then be considered as a user command to <b>overwrite</b> the results provided by the automatics initiated by gDT usage.</p>
|
|
|
|
<p>By default, the following attribute names are used: rhasspyName, rhasspyRoom, rhasspyGroup, rhasspyChannels, rhasspyColors, rhasspySpecials.<br>
|
|
Each of the keywords found in these attributes will be sent by <a href="#RHASSPY-set-update">update</a> to Rhasspy to create the corresponding slot.</p>
|
|
|
|
<ul>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyName"></a><b>rhasspyName</b>
|
|
<p>Comma-separated "labels" for the device as used when speaking a voice-command. They will be used as keywords by Rhasspy. May contain space or mutated vovels.</p>
|
|
<p>Example:<br>
|
|
<code>attr m2_wz_08_sw rhasspyName kitchen lamp,ceiling lamp,workspace,whatever</code></p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyRoom"></a><b>rhasspyRoom</b>
|
|
<p>Comma-separated "labels" for the "rooms" the device is located in. Recommended to be unique.</p>
|
|
<p>Example:<br>
|
|
<code>attr m2_wz_08_sw rhasspyRoom living room</code></p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyGroup"></a><b>rhasspyGroup</b>
|
|
<p>Comma-separated "labels" for the "groups" the device is in. Recommended to be unique.</p>
|
|
<p>Example:
|
|
<code>attr m2_wz_08_sw rhasspyGroup lights</code></p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-Mapping"></a><b>rhasspyMapping</b>
|
|
<p>If automatic detection (gDT) does not work or is not desired, this is the place to tell RHASSPY how your device can be controlled.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr lamp rhasspyMapping SetOnOff:cmdOn=on,cmdOff=off,response="All right"<br>
|
|
GetOnOff:currentVal=state,valueOff=off<br>
|
|
GetNumeric:currentVal=pct,type=brightness<br>
|
|
SetNumeric:currentVal=brightness,cmd=brightness,minVal=0,maxVal=255,map=percent,step=1,type=brightness<br>
|
|
GetState:response=The temperature in the kitchen is at [lamp:temperature] degrees<br>
|
|
MediaControls:cmdPlay=play,cmdPause=pause,cmdStop=stop,cmdBack=previous,cmdFwd=next</code></p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyChannels"></a><b>rhasspyChannels</b>
|
|
<p>Used to change the channels of a tv, set light-scenes, etc.<br>
|
|
<i>key=value</i> line by line arguments mapping command strings to fhem- or Perl commands.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr TV rhasspyChannels orf eins=channel 201<br>
|
|
orf zwei=channel 202<br>
|
|
orf drei=channel 203<br>
|
|
</code></p>
|
|
<p>Note: This attribute is not added to global attribute list by default. Add it using userattr or by editing the global userattr attribute.</p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspyColors"></a><b>rhasspyColors</b>
|
|
<p>Used to change to colors of a light<br>
|
|
<i>key=value</i> line by line arguments mapping keys to setter strings on the same device.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr lamp1 rhasspyColors red=rgb FF0000<br>
|
|
green=rgb 008000<br>
|
|
blue=rgb 0000FF<br>
|
|
yellow=rgb FFFF00</code></p>
|
|
<p>Note: This attribute is not added to global attribute list by default. Add it using userattr or by editing the global userattr attribute.</p>
|
|
</li>
|
|
<li>
|
|
<a id="RHASSPY-attr-rhasspySpecials"></a><b>rhasspySpecials</b>
|
|
<p>Currently some colour light options besides group and venetian blind related stuff is implemented, this could be the place to hold additional options, e.g. for confirmation requests. You may use several of the following lines.</p>
|
|
<p><i>key:value</i> line by line arguments similar to <a href="#RHASSPY-attr-rhasspyTweaks">rhasspyTweaks</a>.</p>
|
|
<ul>
|
|
<li><b>group</b>
|
|
<p>If set, the device will not be directly addressed, but the mentioned group - typically a FHEM <a href="#structure">structure</a> device or a HUEDevice-type group. This has the advantage of saving RF ressources and/or already implemented logics.<br>
|
|
Note: all addressed devices will be switched, even if they are not member of the rhasspyGroup. Each group should only be addressed once, but it's recommended to put this info in all devices under RHASSPY control in the same external group logic.<br>
|
|
All of the following options are optional.</p>
|
|
<ul>
|
|
<li><b>async_delay</b><br>
|
|
Float nummeric value, just as async_delay in structure; the delay will be obeyed prior to the next sending command.</li>
|
|
<li><b>prio</b><br>
|
|
Numeric value, defaults to "0". <i>prio</i> and <i>async_delay</i> will be used to determine the sending order as follows: first devices will be those with lowest prio arg, second sort argument is <i>async_delay</i> with lowest value first.</li>
|
|
</ul>
|
|
<p>Example:</p>
|
|
<p><code>attr lamp1 rhasspySpecials group:async_delay=100 prio=1 group=lights</code></p>
|
|
</li>
|
|
<li><b>venetianBlind</b>
|
|
<p><code>attr blind1 rhasspySpecials venetianBlind:setter=dim device=blind1_slats</code></p>
|
|
<p>Explanation (one of the two arguments is mandatory):
|
|
<ul>
|
|
<li><b>setter</b> is the set command to control slat angle, e.g. <i>positionSlat</i> for CUL_HM or older ZWave type devices</li>
|
|
<li><b>device</b> is needed if the slat command has to be issued towards a different device (applies e.g. to newer ZWave type devices)</li>
|
|
</ul>
|
|
<p>If set, the slat target position will be set to the same level than the main device.</p>
|
|
</li>
|
|
<li><b>colorCommandMap</b>
|
|
<p>Allows mapping of values from the <i>Color></i> key to individual commands.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr lamp1 rhasspySpecials colorCommandMap:0='rgb FF0000' 120='rgb 00FF00' 240='rgb 0000FF'</code></p>
|
|
</li>
|
|
<li><b>colorForceHue2rgb</b>
|
|
<p>Defaults to "0". If set, a rgb command will be issued, even if the device is capable to handle hue commands.</p>
|
|
<p>Example:</p>
|
|
<p><code>attr lamp1 rhasspySpecials colorForceHue2rgb:1</code></p>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
|
|
|
|
<a id="RHASSPY-intents"></a>
|
|
<h4>Intents</h4>
|
|
<p>The following intents are directly implemented in RHASSPY code:
|
|
<ul>
|
|
<li>Shortcuts</li>
|
|
<li>SetOnOff</li>
|
|
<li>SetOnOffGroup</li>
|
|
<li>GetOnOff</li>
|
|
<li>SetNumeric</li>
|
|
<li>SetNumericGroup</li>
|
|
<li>GetNumeric</li>
|
|
<li>GetState</li>
|
|
<li>MediaControls</li>
|
|
<li>MediaChannels</li>
|
|
<li>SetColor</li>
|
|
<li>SetColorGroup</li>
|
|
<li>GetTime</li>
|
|
<li>GetWeekday</li>
|
|
<li>SetTimer</li>
|
|
<li>ConfirmAction</li>
|
|
<li>ReSpeak</li>
|
|
</ul>
|
|
|
|
=end html
|
|
=cut |