[voice] Update dialog processing (#2693)

Related to #2688

Updated methods startDialog
New method stopDialog
Null annotations added to the class DialogProcessor

Allow translation of sentences "said" by the dialog processor in case of error

2 console commands added to start and stop a dialog

Enhanced integration tests

Signed-off-by: Laurent Garnier <lg.hc@free.fr>
pull/2721/head
lolodomo 2022-01-30 18:27:51 +01:00 committed by GitHub
parent 6edc413640
commit 8f0dd94343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 621 additions and 131 deletions

View File

@ -29,6 +29,7 @@ import org.openhab.core.voice.text.InterpretationException;
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added parameter to adjust the volume
* @author Laurent Garnier - Updated methods startDialog and added method stopDialog
*/
@NonNullByDefault
public interface VoiceManager {
@ -121,20 +122,48 @@ public interface VoiceManager {
Voice getPreferredVoice(Set<Voice> voices);
/**
* Starts listening for the keyword that starts a dialog
* Starts an infinite dialog sequence using all default services: keyword spotting on the default audio source,
* audio source listening to retrieve the question, speech to text conversion, interpretation, text to speech
* conversion and playback of the answer on the default audio sink
*
* @throws IllegalStateException if required services are not available
* Only one dialog can be started for the default audio source.
*
* @throws IllegalStateException if required services are not available or the dialog is already started for the
* default audio source
*/
void startDialog();
void startDialog() throws IllegalStateException;
/**
* Starts listening for the keyword that starts a dialog
* Starts an infinite dialog sequence: keyword spotting on the audio source, audio source listening to retrieve
* the question, speech to text conversion, interpretation, text to speech conversion and playback of the answer
* on the audio sink
*
* @throws IllegalStateException if required services are not available
* Only one dialog can be started for an audio source.
*
* @param ks the keyword spotting service to use or null to use the default service
* @param stt the speech-to-text service to use or null to use the default service
* @param tts the text-to-speech service to use or null to use the default service
* @param hli the human language text interpreter to use or null to use the default service
* @param source the audio source to use or null to use the default source
* @param sink the audio sink to use or null to use the default sink
* @param Locale the locale to use or null to use the default locale
* @param keyword the keyword to use during keyword spotting or null to use the default keyword
* @param listeningItem the item to switch ON while listening to a question
* @throws IllegalStateException if required services are not available or the dialog is already started for this
* audio source
*/
void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts,
@Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink,
@Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem);
@Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem)
throws IllegalStateException;
/**
* Stop the dialog associated to an audio source
*
* @param source the audio source or null to consider the default audio source
* @throws IllegalStateException if no dialog is started for the audio source
*/
void stopDialog(@Nullable AudioSource source) throws IllegalStateException;
/**
* Retrieves a TTS service.

View File

@ -18,6 +18,8 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioException;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioSink;
@ -26,6 +28,7 @@ import org.openhab.core.audio.AudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.items.ItemUtil;
import org.openhab.core.items.events.ItemEventFactory;
import org.openhab.core.library.types.OnOffType;
@ -34,6 +37,7 @@ import org.openhab.core.voice.KSEvent;
import org.openhab.core.voice.KSException;
import org.openhab.core.voice.KSListener;
import org.openhab.core.voice.KSService;
import org.openhab.core.voice.KSServiceHandle;
import org.openhab.core.voice.KSpottedEvent;
import org.openhab.core.voice.RecognitionStopEvent;
import org.openhab.core.voice.STTEvent;
@ -48,6 +52,7 @@ import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.Voice;
import org.openhab.core.voice.text.HumanLanguageInterpreter;
import org.openhab.core.voice.text.InterpretationException;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -59,11 +64,28 @@ import org.slf4j.LoggerFactory;
* @author Yannick Schaus - Send commands to an item to indicate the keyword has been spotted
* @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
* @author Christoph Weitkamp - Added parameter to adjust the volume
* @author Laurent Garnier - Added stop() + null annotations + resources releasing
*/
@NonNullByDefault
public class DialogProcessor implements KSListener, STTListener {
private final Logger logger = LoggerFactory.getLogger(DialogProcessor.class);
private final KSService ks;
private final STTService stt;
private final TTSService tts;
private final HumanLanguageInterpreter hli;
private final AudioSource source;
private final AudioSink sink;
private final Locale locale;
private final String keyword;
private final @Nullable String listeningItem;
private final EventPublisher eventPublisher;
private final TranslationProvider i18nProvider;
private final Bundle bundle;
private final @Nullable AudioFormat format;
/**
* If the processor is currently processing a keyword event and thus should not spot further ones.
*/
@ -74,24 +96,15 @@ public class DialogProcessor implements KSListener, STTListener {
*/
private boolean isSTTServerAborting = false;
private STTServiceHandle sttServiceHandle;
private @Nullable KSServiceHandle ksServiceHandle;
private @Nullable STTServiceHandle sttServiceHandle;
private final KSService ks;
private final STTService stt;
private final TTSService tts;
private final HumanLanguageInterpreter hli;
private final AudioSource source;
private final AudioSink sink;
private final Locale locale;
private final String keyword;
private final String listeningItem;
private final EventPublisher eventPublisher;
private final AudioFormat format;
private @Nullable AudioStream streamKS;
private @Nullable AudioStream streamSTT;
public DialogProcessor(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli,
AudioSource source, AudioSink sink, Locale locale, String keyword, String listeningItem,
EventPublisher eventPublisher) {
AudioSource source, AudioSink sink, Locale locale, String keyword, @Nullable String listeningItem,
EventPublisher eventPublisher, TranslationProvider i18nProvider, Bundle bundle) {
this.locale = locale;
this.ks = ks;
this.hli = hli;
@ -102,16 +115,80 @@ public class DialogProcessor implements KSListener, STTListener {
this.keyword = keyword;
this.listeningItem = listeningItem;
this.eventPublisher = eventPublisher;
this.i18nProvider = i18nProvider;
this.bundle = bundle;
this.format = AudioFormat.getBestMatch(source.getSupportedFormats(), sink.getSupportedFormats());
}
public void start() {
AudioFormat fmt = format;
if (fmt == null) {
logger.warn("No compatible audio format found between source '{}' and sink '{}'", source.getId(),
sink.getId());
return;
}
abortKS();
closeStreamKS();
try {
ks.spot(this, source.getInputStream(format), locale, keyword);
} catch (KSException e) {
logger.error("Encountered error calling spot: {}", e.getMessage());
AudioStream stream = source.getInputStream(fmt);
streamKS = stream;
ksServiceHandle = ks.spot(this, stream, locale, keyword);
} catch (AudioException e) {
logger.error("Error creating the audio stream", e);
logger.warn("Error creating the audio stream: {}", e.getMessage());
} catch (KSException e) {
logger.warn("Encountered error calling spot: {}", e.getMessage());
closeStreamKS();
}
}
public void stop() {
abortSTT();
closeStreamSTT();
abortKS();
closeStreamKS();
toggleProcessing(false);
}
private void abortKS() {
KSServiceHandle handle = ksServiceHandle;
if (handle != null) {
handle.abort();
ksServiceHandle = null;
}
}
private void closeStreamKS() {
AudioStream stream = streamKS;
if (stream != null) {
// Due to the current implementation of JavaSoundAudioSource, the stream is not closed as it would
// lead to problem
// try {
// stream.close();
// } catch (IOException e1) {
// }
streamKS = null;
}
}
private void abortSTT() {
STTServiceHandle handle = sttServiceHandle;
if (handle != null) {
handle.abort();
sttServiceHandle = null;
}
isSTTServerAborting = true;
}
private void closeStreamSTT() {
AudioStream stream = streamSTT;
if (stream != null) {
// Due to the current implementation of JavaSoundAudioSource, the stream is not closed as it would
// lead to problem
// try {
// stream.close();
// } catch (IOException e1) {
// }
streamSTT = null;
}
}
@ -120,9 +197,10 @@ public class DialogProcessor implements KSListener, STTListener {
return;
}
processing = value;
if (listeningItem != null && ItemUtil.isValidItemName(listeningItem)) {
String item = listeningItem;
if (item != null && ItemUtil.isValidItemName(item)) {
OnOffType command = (value) ? OnOffType.ON : OnOffType.OFF;
eventPublisher.post(ItemEventFactory.createCommandEvent(listeningItem, command));
eventPublisher.post(ItemEventFactory.createCommandEvent(item, command));
}
}
@ -132,18 +210,34 @@ public class DialogProcessor implements KSListener, STTListener {
isSTTServerAborting = false;
if (ksEvent instanceof KSpottedEvent) {
toggleProcessing(true);
if (stt != null) {
abortSTT();
closeStreamSTT();
isSTTServerAborting = false;
AudioFormat fmt = format;
if (fmt != null) {
try {
sttServiceHandle = stt.recognize(this, source.getInputStream(format), locale, new HashSet<>());
} catch (STTException e) {
say("Error during recognition: " + e.getMessage());
AudioStream stream = source.getInputStream(fmt);
streamSTT = stream;
sttServiceHandle = stt.recognize(this, stream, locale, new HashSet<>());
} catch (AudioException e) {
logger.error("Error creating the audio stream", e);
logger.warn("Error creating the audio stream: {}", e.getMessage());
toggleProcessing(false);
} catch (STTException e) {
closeStreamSTT();
toggleProcessing(false);
String msg = e.getMessage();
String text = i18nProvider.getText(bundle, "error.stt-exception", null, locale);
if (msg != null) {
say(text == null ? msg : text.replace("{0}", msg));
} else if (text != null) {
say(text.replace("{0}", ""));
}
}
}
} else if (ksEvent instanceof KSErrorEvent) {
KSErrorEvent kse = (KSErrorEvent) ksEvent;
say("Encountered error spotting keywords, " + kse.getMessage());
String text = i18nProvider.getText(bundle, "error.ks-error", null, locale);
say(text == null ? kse.getMessage() : text.replace("{0}", kse.getMessage()));
}
}
}
@ -152,8 +246,7 @@ public class DialogProcessor implements KSListener, STTListener {
public synchronized void sttEventReceived(STTEvent sttEvent) {
if (sttEvent instanceof SpeechRecognitionEvent) {
if (!isSTTServerAborting) {
sttServiceHandle.abort();
isSTTServerAborting = true;
abortSTT();
SpeechRecognitionEvent sre = (SpeechRecognitionEvent) sttEvent;
String question = sre.getTranscript();
try {
@ -163,18 +256,21 @@ public class DialogProcessor implements KSListener, STTListener {
say(answer);
}
} catch (InterpretationException e) {
say(e.getMessage());
String msg = e.getMessage();
if (msg != null) {
say(msg);
}
}
}
} else if (sttEvent instanceof RecognitionStopEvent) {
toggleProcessing(false);
} else if (sttEvent instanceof SpeechRecognitionErrorEvent) {
if (!isSTTServerAborting) {
sttServiceHandle.abort();
isSTTServerAborting = true;
abortSTT();
toggleProcessing(false);
SpeechRecognitionErrorEvent sre = (SpeechRecognitionErrorEvent) sttEvent;
say("Encountered error: " + sre.getMessage());
String text = i18nProvider.getText(bundle, "error.stt-error", null, locale);
say(text == null ? sre.getMessage() : text.replace("{0}", sre.getMessage()));
}
}
}
@ -210,13 +306,21 @@ public class DialogProcessor implements KSListener, STTListener {
try {
sink.process(audioStream);
} catch (UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
logger.warn("Error saying '{}': {}", text, e.getMessage(), e);
if (logger.isDebugEnabled()) {
logger.debug("Error saying '{}': {}", text, e.getMessage(), e);
} else {
logger.warn("Error saying '{}': {}", text, e.getMessage());
}
}
} else {
logger.warn("Failed playing audio stream '{}' as audio doesn't support it.", audioStream);
}
} catch (TTSException e) {
logger.error("Error saying '{}': {}", text, e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Error saying '{}': {}", text, e.getMessage(), e);
} else {
logger.warn("Error saying '{}': {}", text, e.getMessage());
}
}
}
}

View File

@ -15,9 +15,13 @@ package org.openhab.core.voice.internal;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.audio.AudioManager;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.audio.AudioSource;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.io.console.Console;
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension;
@ -29,6 +33,7 @@ import org.openhab.core.items.ItemRegistry;
import org.openhab.core.voice.TTSService;
import org.openhab.core.voice.Voice;
import org.openhab.core.voice.VoiceManager;
import org.openhab.core.voice.text.HumanLanguageInterpreter;
import org.openhab.core.voice.text.InterpretationException;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
@ -39,6 +44,7 @@ import org.osgi.service.component.annotations.Reference;
*
* @author Kai Kreuzer - Initial contribution
* @author Wouter Born - Sort TTS voices
* @author Laurent Garnier - Added sub-commands startdialog and stopdialog
*/
@Component(service = ConsoleCommandExtension.class)
@NonNullByDefault
@ -47,16 +53,21 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
private static final String SUBCMD_SAY = "say";
private static final String SUBCMD_INTERPRET = "interpret";
private static final String SUBCMD_VOICES = "voices";
private static final String SUBCMD_START_DIALOG = "startdialog";
private static final String SUBCMD_STOP_DIALOG = "stopdialog";
private final ItemRegistry itemRegistry;
private final VoiceManager voiceManager;
private final AudioManager audioManager;
private final LocaleProvider localeProvider;
@Activate
public VoiceConsoleCommandExtension(final @Reference VoiceManager voiceManager,
final @Reference LocaleProvider localeProvider, final @Reference ItemRegistry itemRegistry) {
final @Reference AudioManager audioManager, final @Reference LocaleProvider localeProvider,
final @Reference ItemRegistry itemRegistry) {
super("voice", "Commands around voice enablement features.");
this.voiceManager = voiceManager;
this.audioManager = audioManager;
this.localeProvider = localeProvider;
this.itemRegistry = itemRegistry;
}
@ -65,7 +76,11 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
public List<String> getUsages() {
return List.of(buildCommandUsage(SUBCMD_SAY + " <text>", "speaks a text"),
buildCommandUsage(SUBCMD_INTERPRET + " <command>", "interprets a human language command"),
buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services"));
buildCommandUsage(SUBCMD_VOICES, "lists available voices of the TTS services"),
buildCommandUsage(SUBCMD_START_DIALOG + " [<source> <interpreter> <sink> <keyword>]",
"start a new dialog processing using the default services or the services identified with provided arguments"),
buildCommandUsage(SUBCMD_STOP_DIALOG + " [<source>]",
"stop the dialog processing for the default audio source or the audio source identified with provided argument"));
}
@Override
@ -99,6 +114,24 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
}
}
return;
case SUBCMD_START_DIALOG:
try {
AudioSource source = args.length < 2 ? null : getSource(args[1]);
HumanLanguageInterpreter hli = args.length < 3 ? null : voiceManager.getHLI(args[2]);
AudioSink sink = args.length < 4 ? null : audioManager.getSink(args[3]);
String keyword = args.length < 5 ? null : args[4];
voiceManager.startDialog(null, null, null, hli, source, sink, null, keyword, null);
} catch (IllegalStateException e) {
console.println(e.getMessage());
}
break;
case SUBCMD_STOP_DIALOG:
try {
voiceManager.stopDialog(args.length < 2 ? null : getSource(args[1]));
} catch (IllegalStateException e) {
console.println(e.getMessage());
}
break;
default:
break;
}
@ -158,4 +191,14 @@ public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtensio
}
voiceManager.say(msg.toString());
}
private @Nullable AudioSource getSource(@Nullable String sourceId) {
Set<AudioSource> sources = audioManager.getAllSources();
for (AudioSource source : sources) {
if (source.getId().equals(sourceId)) {
return source;
}
}
return null;
}
}

View File

@ -42,6 +42,7 @@ import org.openhab.core.config.core.ConfigurableService;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.voice.KSService;
import org.openhab.core.voice.STTService;
@ -51,9 +52,12 @@ import org.openhab.core.voice.Voice;
import org.openhab.core.voice.VoiceManager;
import org.openhab.core.voice.text.HumanLanguageInterpreter;
import org.openhab.core.voice.text.InterpretationException;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
@ -69,6 +73,7 @@ import org.slf4j.LoggerFactory;
* @author Christoph Weitkamp - Added getSupportedStreams() and UnsupportedAudioStreamException
* @author Christoph Weitkamp - Added parameter to adjust the volume
* @author Wouter Born - Sort TTS options
* @author Laurent Garnier - Updated methods startDialog and added method stopDialog
*/
@Component(immediate = true, configurationPid = VoiceManagerImpl.CONFIGURATION_PID, //
property = Constants.SERVICE_PID + "=org.openhab.voice")
@ -103,6 +108,9 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
private final LocaleProvider localeProvider;
private final AudioManager audioManager;
private final EventPublisher eventPublisher;
private final TranslationProvider i18nProvider;
private @Nullable Bundle bundle;
/**
* default settings filled through the service configuration
@ -116,19 +124,28 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
private @Nullable String defaultVoice;
private final Map<String, String> defaultVoices = new HashMap<>();
private Map<String, DialogProcessor> dialogProcessors = new HashMap<>();
@Activate
public VoiceManagerImpl(final @Reference LocaleProvider localeProvider, final @Reference AudioManager audioManager,
final @Reference EventPublisher eventPublisher) {
final @Reference EventPublisher eventPublisher, final @Reference TranslationProvider i18nProvider) {
this.localeProvider = localeProvider;
this.audioManager = audioManager;
this.eventPublisher = eventPublisher;
this.i18nProvider = i18nProvider;
}
@Activate
protected void activate(Map<String, Object> config) {
protected void activate(BundleContext bundleContext, Map<String, Object> config) {
this.bundle = bundleContext.getBundle();
modified(config);
}
@Deactivate
protected void deactivate() {
stopAllDialogs();
}
@SuppressWarnings("null")
@Modified
protected void modified(Map<String, Object> config) {
@ -257,7 +274,11 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
}
}
} catch (TTSException | UnsupportedAudioFormatException | UnsupportedAudioStreamException e) {
logger.warn("Error saying '{}': {}", text, e.getMessage(), e);
if (logger.isDebugEnabled()) {
logger.debug("Error saying '{}': {}", text, e.getMessage(), e);
} else {
logger.warn("Error saying '{}': {}", text, e.getMessage());
}
}
}
@ -458,38 +479,72 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider {
}
@Override
public void startDialog() {
public void startDialog() throws IllegalStateException {
startDialog(null, null, null, null, null, null, null, this.keyword, this.listeningItem);
}
@Override
public void startDialog(@Nullable KSService ksService, @Nullable STTService sttService,
@Nullable TTSService ttsService, @Nullable HumanLanguageInterpreter interpreter,
@Nullable AudioSource audioSource, @Nullable AudioSink audioSink, @Nullable Locale locale,
@Nullable String keyword, @Nullable String listeningItem) {
public void startDialog(@Nullable KSService ks, @Nullable STTService stt, @Nullable TTSService tts,
@Nullable HumanLanguageInterpreter hli, @Nullable AudioSource source, @Nullable AudioSink sink,
@Nullable Locale locale, @Nullable String keyword, @Nullable String listeningItem)
throws IllegalStateException {
// use defaults, if null
KSService ks = (ksService == null) ? getKS() : ksService;
STTService stt = (sttService == null) ? getSTT() : sttService;
TTSService tts = (ttsService == null) ? getTTS() : ttsService;
HumanLanguageInterpreter hli = (interpreter == null) ? getHLI() : interpreter;
AudioSource source = (audioSource == null) ? audioManager.getSource() : audioSource;
AudioSink sink = (audioSink == null) ? audioManager.getSink() : audioSink;
KSService ksService = (ks == null) ? getKS() : ks;
STTService sttService = (stt == null) ? getSTT() : stt;
TTSService ttsService = (tts == null) ? getTTS() : tts;
HumanLanguageInterpreter interpreter = (hli == null) ? getHLI() : hli;
AudioSource audioSource = (source == null) ? audioManager.getSource() : source;
AudioSink audioSink = (sink == null) ? audioManager.getSink() : sink;
Locale loc = (locale == null) ? localeProvider.getLocale() : locale;
String kw = (keyword == null) ? this.keyword : keyword;
String item = (listeningItem == null) ? this.listeningItem : listeningItem;
Bundle b = bundle;
if (ks != null && stt != null && tts != null && hli != null && source != null && sink != null && loc != null
&& kw != null) {
DialogProcessor processor = new DialogProcessor(ks, stt, tts, hli, source, sink, loc, kw, item,
this.eventPublisher);
processor.start();
if (ksService != null && sttService != null && ttsService != null && interpreter != null && audioSource != null
&& audioSink != null && b != null) {
DialogProcessor processor = dialogProcessors.get(audioSource.getId());
if (processor == null) {
logger.debug("Starting a new dialog for source {} ({})", audioSource.getLabel(null),
audioSource.getId());
processor = new DialogProcessor(ksService, sttService, ttsService, interpreter, audioSource, audioSink,
loc, kw, item, this.eventPublisher, this.i18nProvider, b);
dialogProcessors.put(audioSource.getId(), processor);
processor.start();
} else {
throw new IllegalStateException(
String.format("Cannot start dialog as a dialog is already started for audio source '%s'.",
audioSource.getLabel(null)));
}
} else {
String msg = "Cannot start dialog as services are missing.";
logger.error(msg);
throw new IllegalStateException(msg);
throw new IllegalStateException("Cannot start dialog as services are missing.");
}
}
@Override
public void stopDialog(@Nullable AudioSource source) throws IllegalStateException {
AudioSource audioSource = (source == null) ? audioManager.getSource() : source;
if (audioSource != null) {
DialogProcessor processor = dialogProcessors.remove(audioSource.getId());
if (processor != null) {
processor.stop();
logger.debug("Dialog stopped for source {} ({})", audioSource.getLabel(null), audioSource.getId());
} else {
throw new IllegalStateException(
String.format("Cannot stop dialog as no dialog is started for audio source '%s'.",
audioSource.getLabel(null)));
}
} else {
throw new IllegalStateException("Cannot stop dialog as audio source is missing.");
}
}
private void stopAllDialogs() {
dialogProcessors.values().forEach(processor -> {
processor.stop();
});
dialogProcessors.clear();
}
@Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC)
protected void addKSService(KSService ksService) {
this.ksServices.put(ksService.getId(), ksService);

View File

@ -14,3 +14,7 @@ system.config.voice.listeningItem.label = Listening Switch
system.config.voice.listeningItem.description = If provided, the item will be switched on during the period when the dialog processor has spotted the keyword and is listening for commands.
service.system.voice.label = Voice
error.ks-error = Encountered error while spotting keywords, {0}
error.stt-error = Encountered error while recognizing text, {0}
error.stt-exception = Error during recognition, {0}

View File

@ -30,22 +30,20 @@ import org.openhab.core.voice.text.InterpretationException;
public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter {
private static final String INTERPRETED_TEXT = "Interpreted text";
private static final String EXCEPTION_MESSAGE = "Exception message";
private static final String EXCEPTION_MESSAGE = "interpretation exception";
private static final String HLI_STUB_ID = "HLIStubID";
private static final String HLI_STUB_LABEL = "HLIStubLabel";
private boolean isInterpretationExceptionExpected;
private boolean exceptionExpected;
private String question = "";
private String answer = "";
@Override
public String getId() {
return HLI_STUB_ID;
}
public void setIsInterpretationExceptionExpected(boolean value) {
isInterpretationExceptionExpected = value;
}
@Override
public String getLabel(Locale locale) {
return HLI_STUB_LABEL;
@ -53,10 +51,12 @@ public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter {
@Override
public String interpret(Locale locale, String text) throws InterpretationException {
if (isInterpretationExceptionExpected) {
question = text;
if (exceptionExpected) {
throw new InterpretationException(EXCEPTION_MESSAGE);
} else {
return INTERPRETED_TEXT;
answer = INTERPRETED_TEXT;
return answer;
}
}
@ -78,6 +78,18 @@ public class HumanLanguageInterpreterStub implements HumanLanguageInterpreter {
return null;
}
public void setExceptionExpected(boolean exceptionExpected) {
this.exceptionExpected = exceptionExpected;
}
public String getQuestion() {
return question;
}
public String getAnswer() {
return answer;
}
@Override
public String toString() {
return getId();

View File

@ -17,10 +17,12 @@ import java.util.Set;
import org.openhab.core.audio.AudioFormat;
import org.openhab.core.audio.AudioStream;
import org.openhab.core.voice.KSErrorEvent;
import org.openhab.core.voice.KSException;
import org.openhab.core.voice.KSListener;
import org.openhab.core.voice.KSService;
import org.openhab.core.voice.KSServiceHandle;
import org.openhab.core.voice.KSpottedEvent;
/**
* A {@link KSService} stub used for the tests.
@ -35,18 +37,19 @@ public class KSServiceStub implements KSService {
private static final String KSSERVICE_STUB_ID = "ksServiceStubID";
private static final String KSSERVICE_STUB_LABEL = "ksServiceStubLabel";
private static final String EXCEPTION_MESSAGE = "keyword spotting exception";
private static final String ERROR_MESSAGE = "keyword spotting error";
private boolean exceptionExpected;
private boolean errorExpected;
private boolean isWordSpotted;
private boolean isKSExceptionExpected;
private boolean aborted;
@Override
public String getId() {
return KSSERVICE_STUB_ID;
}
public void setIsKsExceptionExpected(boolean value) {
this.isKSExceptionExpected = value;
}
@Override
public String getLabel(Locale locale) {
return KSSERVICE_STUB_LABEL;
@ -65,22 +68,40 @@ public class KSServiceStub implements KSService {
@Override
public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword)
throws KSException {
if (isKSExceptionExpected) {
throw new KSException("Expected KSException");
if (exceptionExpected) {
throw new KSException(EXCEPTION_MESSAGE);
} else {
isWordSpotted = true;
if (errorExpected) {
ksListener.ksEventReceived(new KSErrorEvent(ERROR_MESSAGE));
} else {
isWordSpotted = true;
ksListener.ksEventReceived(new KSpottedEvent());
}
return new KSServiceHandle() {
@Override
public void abort() {
aborted = true;
}
};
}
}
public void setExceptionExpected(boolean exceptionExpected) {
this.exceptionExpected = exceptionExpected;
}
public void setErrorExpected(boolean errorExpected) {
this.errorExpected = errorExpected;
}
public boolean isWordSpotted() {
return isWordSpotted;
}
public boolean isAborted() {
return aborted;
}
@Override
public String toString() {
return getId();

View File

@ -23,6 +23,8 @@ import org.openhab.core.voice.STTException;
import org.openhab.core.voice.STTListener;
import org.openhab.core.voice.STTService;
import org.openhab.core.voice.STTServiceHandle;
import org.openhab.core.voice.SpeechRecognitionErrorEvent;
import org.openhab.core.voice.SpeechRecognitionEvent;
/**
* A {@link STTService} stub used for the tests.
@ -38,6 +40,14 @@ public class STTServiceStub implements STTService {
private static final String STTSERVICE_STUB_ID = "sttServiceStubID";
private static final String STTSERVICE_STUB_LABEL = "sttServiceStubLabel";
private static final String RECOGNIZED_TEXT = "Recognized text";
private static final String EXCEPTION_MESSAGE = "STT exception";
private static final String ERROR_MESSAGE = "STT error";
private boolean exceptionExpected;
private boolean errorExpected;
private boolean recognized;
@Override
public String getId() {
return STTSERVICE_STUB_ID;
@ -61,12 +71,34 @@ public class STTServiceStub implements STTService {
@Override
public STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale,
Set<String> grammars) throws STTException {
return new STTServiceHandle() {
// this method will not be used in the tests
@Override
public void abort() {
if (exceptionExpected) {
throw new STTException(EXCEPTION_MESSAGE);
} else {
if (errorExpected) {
sttListener.sttEventReceived(new SpeechRecognitionErrorEvent(ERROR_MESSAGE));
} else {
recognized = true;
sttListener.sttEventReceived(new SpeechRecognitionEvent(RECOGNIZED_TEXT, 0.75f));
}
};
return new STTServiceHandle() {
// this method will not be used in the tests
@Override
public void abort() {
}
};
}
}
public void setExceptionExpected(boolean exceptionExpected) {
this.exceptionExpected = exceptionExpected;
}
public void setErrorExpected(boolean errorExpected) {
this.errorExpected = errorExpected;
}
public boolean isRecognized() {
return recognized;
}
@Override

View File

@ -44,6 +44,7 @@ public class TTSServiceStub implements TTSService {
private static final String TTS_SERVICE_STUB_LABEL = "ttsServiceStubLabel";
private @Nullable BundleContext context;
private String synthesized = "";
public TTSServiceStub() {
}
@ -88,6 +89,7 @@ public class TTSServiceStub implements TTSService {
@Override
public AudioStream synthesize(String text, Voice voice, final AudioFormat requestedFormat) throws TTSException {
synthesized = text;
return new AudioStream() {
@Override
@ -103,6 +105,10 @@ public class TTSServiceStub implements TTSService {
};
}
public String getSynthesized() {
return synthesized;
}
@Override
public String toString() {
return getId();

View File

@ -29,6 +29,8 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.audio.AudioManager;
import org.openhab.core.config.core.ParameterOption;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.test.java.JavaOSGiTest;
import org.openhab.core.voice.Voice;
import org.openhab.core.voice.VoiceManager;
@ -44,14 +46,19 @@ import org.osgi.service.cm.ConfigurationAdmin;
* @author Velin Yordanov - migrated tests from groovy to java
*/
public class VoiceManagerImplTest extends JavaOSGiTest {
private static final String CONFIG_DEFAULT_SINK = "defaultSink";
private static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
private static final String CONFIG_DEFAULT_HLI = "defaultHLI";
private static final String CONFIG_DEFAULT_KS = "defaultKS";
private static final String CONFIG_DEFAULT_STT = "defaultSTT";
private static final String CONFIG_DEFAULT_VOICE = "defaultVoice";
private static final String CONFIG_DEFAULT_TTS = "defaultTTS";
private static final String CONFIG_KEYWORD = "keyword";
private static final String CONFIG_LANGUAGE = "language";
private VoiceManagerImpl voiceManager;
private AudioManager audioManager;
private LocaleProvider localeProvider;
private TranslationProvider i18nProvider;
private SinkStub sink;
private TTSServiceStub ttsService;
private VoiceStub voice;
@ -70,6 +77,7 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
registerService(sink);
registerService(voice);
registerService(source);
ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class);
@ -77,11 +85,24 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
assertNotNull(audioManager);
Dictionary<String, Object> audioConfig = new Hashtable<>();
audioConfig.put("defaultSink", sink.getId());
audioConfig.put(CONFIG_DEFAULT_SINK, sink.getId());
audioConfig.put(CONFIG_DEFAULT_SOURCE, source.getId());
Configuration configuration = configAdmin.getConfiguration("org.openhab.audio", null);
configuration.update(audioConfig);
configuration.update();
localeProvider = getService(LocaleProvider.class);
assertNotNull(localeProvider);
Dictionary<String, Object> localeConfig = new Hashtable<>();
localeConfig.put(CONFIG_LANGUAGE, Locale.ENGLISH.getLanguage());
configuration = configAdmin.getConfiguration("org.openhab.i18n", null);
configuration.update(localeConfig);
configuration.update();
i18nProvider = getService(TranslationProvider.class);
assertNotNull(i18nProvider);
voiceManager = getService(VoiceManager.class, VoiceManagerImpl.class);
assertNotNull(voiceManager);
@ -94,34 +115,27 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
@Test
public void saySomethingWhenTheDefaultTTSIsSetAndItIsARegisteredService() {
registerService(ttsService);
voiceManager.say("hello", null, sink.getId());
voiceManager.say("hello Jack", null, sink.getId());
assertTrue(sink.getIsStreamProcessed());
}
@Test
public void saySomethingWithAGivenVoiceIdWhenTheDefaultTTSIsSetAndItIsARegisteredService() {
registerService(ttsService);
voiceManager.say("hello", voice.getUID(), sink.getId());
voiceManager.say("hello John", voice.getUID(), sink.getId());
assertTrue(sink.getIsStreamProcessed());
}
@Test
public void saySomethingWhenTheVoiceIdIsNotFullyQualifiedTheDefaultTtsIsSetAndItIsARegisteredService() {
registerService(ttsService);
voiceManager.say("hello", "anotherVoiceId", sink.getId());
voiceManager.say("hello Kate", "anotherVoiceId", sink.getId());
assertFalse(sink.getIsStreamProcessed());
}
@Test
public void testTheVoiceManagerWithAGivenVoiceIdAndSinkIdWhenTheDefaultTtsIsSetAndItIsARegisteredService() {
registerService(ttsService);
voiceManager.say("hello", voice.getUID(), sink.getId());
assertTrue(sink.getIsStreamProcessed());
}
@Test
public void saySomethingWhenTheDefaultTtsIsSetButItIsNotARegisteredService() {
voiceManager.say("hello", null, sink.getId());
voiceManager.say("hello Jennifer", null, sink.getId());
assertFalse(sink.getIsStreamProcessed());
}
@ -153,7 +167,13 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
Configuration configuration = configAdmin.getConfiguration(VoiceManagerImpl.CONFIGURATION_PID);
configuration.update(voiceConfig);
String result = voiceManager.interpret("something", hliStub.getId());
// Wait some time to be sure that the configuration will be updated
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
String result = voiceManager.interpret("something", null);
assertThat(result, is("Interpreted text"));
}
@ -161,69 +181,181 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
public void verifyThatADialogIsNotStartedWhenAnyOfTheRequiredServiceIsNull() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
ttsService = null;
hliStub = new HumanLanguageInterpreterStub();
source = new AudioSourceStub();
assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService,
hliStub, source, sink, Locale.getDefault(), "word", null));
assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, null, hliStub,
source, sink, Locale.ENGLISH, "word", null));
assertFalse(ksService.isWordSpotted());
assertFalse(sink.getIsStreamProcessed());
}
@Test
public void startDialogWhenAllOfTheRequiredServicesAreAvailable() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
ttsService = new TTSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
source = new AudioSourceStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
registerService(source);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, null, "word", null);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertTrue(ksService.isWordSpotted());
assertTrue(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is("Recognized text"));
assertThat(hliStub.getAnswer(), is("Interpreted text"));
assertThat(ttsService.getSynthesized(), is("Interpreted text"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
assertTrue(ksService.isAborted());
}
@Test
public void startDialogAndVerifyThatAKSExceptionIsProperlyHandled() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
ttsService = new TTSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
source = new AudioSourceStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
registerService(source);
ksService.setIsKsExceptionExpected(true);
ksService.setExceptionExpected(true);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, null, "", null);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "", null);
assertFalse(ksService.isWordSpotted());
assertFalse(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is(""));
assertThat(hliStub.getAnswer(), is(""));
assertThat(ttsService.getSynthesized(), is(""));
assertFalse(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
}
@Test
public void startDialogAndVerifyThatAKSErrorIsProperlyHandled() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
ksService.setErrorExpected(true);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertFalse(ksService.isWordSpotted());
assertFalse(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is(""));
assertThat(hliStub.getAnswer(), is(""));
assertThat(ttsService.getSynthesized(),
is("Encountered error while spotting keywords, keyword spotting error"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
}
@Test
public void startDialogAndVerifyThatASTTExceptionIsProperlyHandled() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
sttService.setExceptionExpected(true);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertTrue(ksService.isWordSpotted());
assertFalse(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is(""));
assertThat(hliStub.getAnswer(), is(""));
assertThat(ttsService.getSynthesized(), is("Error during recognition, STT exception"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
}
@Test
public void startDialogAndVerifyThatASpeechRecognitionErrorIsProperlyHandled() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
sttService.setErrorExpected(true);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertTrue(ksService.isWordSpotted());
assertFalse(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is(""));
assertThat(hliStub.getAnswer(), is(""));
assertThat(ttsService.getSynthesized(), is("Encountered error while recognizing text, STT error"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
}
@Test
public void startDialogAndVerifyThatAnInterpretationExceptionIsProperlyHandled() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
hliStub.setExceptionExpected(true);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertTrue(ksService.isWordSpotted());
assertTrue(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is("Recognized text"));
assertThat(hliStub.getAnswer(), is(""));
assertThat(ttsService.getSynthesized(), is("interpretation exception"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(source);
}
@Test
public void startDialogWithoutPassingAnyParameters() throws IOException, InterruptedException {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
ttsService = new TTSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
source = new AudioSourceStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
registerService(source);
Dictionary<String, Object> config = new Hashtable<>();
config.put(CONFIG_KEYWORD, "word");
@ -236,16 +368,50 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
Configuration configuration = configAdmin.getConfiguration(VoiceManagerImpl.CONFIGURATION_PID);
configuration.update(config);
waitForAssert(() -> {
try {
voiceManager.startDialog();
} catch (Exception ex) {
// if the configuration is not updated yet the startDialog method will throw and exception which will
// break the test
}
// Wait some time to be sure that the configuration will be updated
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
assertTrue(ksService.isWordSpotted());
});
voiceManager.startDialog();
assertTrue(ksService.isWordSpotted());
assertTrue(sttService.isRecognized());
assertThat(hliStub.getQuestion(), is("Recognized text"));
assertThat(hliStub.getAnswer(), is("Interpreted text"));
assertThat(ttsService.getSynthesized(), is("Interpreted text"));
assertTrue(sink.getIsStreamProcessed());
voiceManager.stopDialog(null);
assertTrue(ksService.isAborted());
}
@Test
public void verifyThatOnlyOneDialogPerSourceIsPossible() {
sttService = new STTServiceStub();
ksService = new KSServiceStub();
hliStub = new HumanLanguageInterpreterStub();
registerService(sttService);
registerService(ksService);
registerService(ttsService);
registerService(hliStub);
voiceManager.startDialog(ksService, sttService, ttsService, hliStub, source, sink, Locale.ENGLISH, "word",
null);
assertTrue(ksService.isWordSpotted());
assertThrows(IllegalStateException.class, () -> voiceManager.startDialog(ksService, sttService, ttsService,
hliStub, source, sink, Locale.ENGLISH, "word", null));
voiceManager.stopDialog(source);
assertTrue(ksService.isAborted());
assertThrows(IllegalStateException.class, () -> voiceManager.stopDialog(source));
}
@Test
@ -352,7 +518,7 @@ public class VoiceManagerImplTest extends JavaOSGiTest {
BundleContext context = bundleContext;
ttsService = new TTSServiceStub(context);
registerService(ttsService);
Locale locale = Locale.getDefault();
Locale locale = localeProvider.getLocale();
boolean isVoiceStubInTheOptions = false;

View File

@ -43,7 +43,6 @@ public class VoiceStub implements Voice {
@Override
public Locale getLocale() {
// we need to return something different from null here (the real value is not important)
return Locale.getDefault();
return Locale.ENGLISH;
}
}

View File

@ -41,7 +41,7 @@ public class InterpretCommandTest extends VoiceConsoleCommandExtensionTest {
private static final String CONFIG_DEFAULT_VOICE = "defaultVoice";
private static final String SUBCMD_INTERPRET = "interpret";
private static final String INTERPRETED_TEXT = "Interpreted text";
private static final String EXCEPTION_MESSAGE = "Exception message";
private static final String EXCEPTION_MESSAGE = "interpretation exception";
private HumanLanguageInterpreterStub hliStub;
private VoiceStub voice;
@ -79,7 +79,7 @@ public class InterpretCommandTest extends VoiceConsoleCommandExtensionTest {
@Test
public void verifyThatAnInterpretationExceptionIsHandledProperly() {
waitForAssert(() -> {
hliStub.setIsInterpretationExceptionExpected(true);
hliStub.setExceptionExpected(true);
String[] params = new String[] { SUBCMD_INTERPRET, "text", "to", "be", "interpreted" };
extensionService.execute(params, console);
assertThat(console.getPrintedText(), is(EXCEPTION_MESSAGE));

View File

@ -14,15 +14,22 @@ package org.openhab.core.voice.voiceconsolecommandextension;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Locale;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.voice.internal.SinkStub;
import org.openhab.core.voice.internal.TTSServiceStub;
import org.openhab.core.voice.internal.VoiceStub;
import org.osgi.framework.BundleContext;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
/**
* A {@link VoiceConsoleCommandExtensionTest} which tests the execution of the command "voices".
@ -31,13 +38,25 @@ import org.osgi.framework.BundleContext;
* @author Velin Yordanov - migrated tests from groovy to java
*/
public class VoicesCommandTest extends VoiceConsoleCommandExtensionTest {
private static final String CONFIG_LANGUAGE = "language";
private static final String SUBCMD_VOICES = "voices";
private LocaleProvider localeProvider;
private TTSServiceStub ttsService;
private SinkStub sink;
private VoiceStub voice;
@BeforeEach
public void setUp() {
public void setUp() throws IOException {
localeProvider = getService(LocaleProvider.class);
assertNotNull(localeProvider);
Dictionary<String, Object> localeConfig = new Hashtable<>();
localeConfig.put(CONFIG_LANGUAGE, Locale.ENGLISH.getLanguage());
ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class);
Configuration configuration = configAdmin.getFactoryConfiguration("org.openhab.i18n", null);
configuration.update(localeConfig);
configuration.update();
BundleContext context = bundleContext;
ttsService = new TTSServiceStub(context);
@ -52,7 +71,7 @@ public class VoicesCommandTest extends VoiceConsoleCommandExtensionTest {
@Test
public void testVoicesCommand() {
String[] command = new String[] { SUBCMD_VOICES };
Locale locale = Locale.getDefault();
Locale locale = localeProvider.getLocale();
String expectedText = String.format("* %s - %s - %s (%s)", ttsService.getLabel(locale),
voice.getLocale().getDisplayName(locale), voice.getLabel(), voice.getUID());
extensionService.execute(command, console);