From c30e4b8eea82e652d66d7e04a4afcaa23e92c3ff Mon Sep 17 00:00:00 2001 From: GiviMAD Date: Wed, 8 Feb 2023 12:12:22 -0800 Subject: [PATCH] [voice] Support managed dialogs (#3264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [voice] introduce methods for dialog persistency Signed-off-by: Miguel Álvarez --- bundles/org.openhab.core.voice/pom.xml | 11 + .../org/openhab/core/voice/DialogContext.java | 40 +++- .../core/voice/DialogRegistration.java | 81 +++++++ .../org/openhab/core/voice/VoiceManager.java | 32 +++ .../core/voice/internal/DialogProcessor.java | 10 +- .../core/voice/internal/VoiceManagerImpl.java | 199 ++++++++++++++++-- .../org.openhab.core.voice.tests/itest.bndrun | 1 + .../voice/internal/VoiceManagerImplTest.java | 54 ++++- .../InterpretCommandTest.java | 1 + .../SayCommandTest.java | 1 + .../VoiceConsoleCommandExtensionTest.java | 1 + .../VoicesCommandTest.java | 1 + 12 files changed, 395 insertions(+), 37 deletions(-) create mode 100644 bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogRegistration.java diff --git a/bundles/org.openhab.core.voice/pom.xml b/bundles/org.openhab.core.voice/pom.xml index dd06334e0bc..8b7fe260222 100644 --- a/bundles/org.openhab.core.voice/pom.xml +++ b/bundles/org.openhab.core.voice/pom.xml @@ -30,6 +30,17 @@ org.openhab.core.io.console ${project.version} + + org.openhab.core.bundles + org.openhab.core + ${project.version} + + + org.openhab.core.bundles + org.openhab.core.test + ${project.version} + test + diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogContext.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogContext.java index 58e8cf7fdbd..417e00afeb5 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogContext.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogContext.java @@ -137,17 +137,23 @@ public Builder withSink(@Nullable AudioSink sink) { } public Builder withKS(@Nullable KSService service) { - this.ks = service; + if (service != null) { + this.ks = service; + } return this; } public Builder withSTT(@Nullable STTService service) { - this.stt = service; + if (service != null) { + this.stt = service; + } return this; } public Builder withTTS(@Nullable TTSService service) { - this.tts = service; + if (service != null) { + this.tts = service; + } return this; } @@ -156,32 +162,44 @@ public Builder withHLIs(Collection services) { } public Builder withHLIs(List services) { - this.hlis = services; + if (!services.isEmpty()) { + this.hlis = services; + } return this; } - public Builder withKeyword(String keyword) { - this.keyword = keyword; + public Builder withKeyword(@Nullable String keyword) { + if (keyword != null && !keyword.isBlank()) { + this.keyword = keyword; + } return this; } public Builder withVoice(@Nullable Voice voice) { - this.voice = voice; + if (voice != null) { + this.voice = voice; + } return this; } public Builder withListeningItem(@Nullable String listeningItem) { - this.listeningItem = listeningItem; + if (listeningItem != null) { + this.listeningItem = listeningItem; + } return this; } public Builder withMelody(@Nullable String listeningMelody) { - this.listeningMelody = listeningMelody; + if (listeningMelody != null) { + this.listeningMelody = listeningMelody; + } return this; } - public Builder withLocale(Locale locale) { - this.locale = locale; + public Builder withLocale(@Nullable Locale locale) { + if (locale != null) { + this.locale = locale; + } return this; } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogRegistration.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogRegistration.java new file mode 100644 index 00000000000..12421a7e480 --- /dev/null +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/DialogRegistration.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.voice; + +import java.util.List; +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Describes dialog desired services and options. + * + * @author Miguel Álvarez - Initial contribution + */ +@NonNullByDefault +public class DialogRegistration { + /** + * Dialog audio source id + */ + public String sourceId; + /** + * Dialog audio sink id + */ + public String sinkId; + /** + * Preferred keyword-spotting service + */ + public @Nullable String ksId; + /** + * Selected keyword for spotting + */ + public @Nullable String keyword; + /** + * Preferred speech-to-text service id + */ + public @Nullable String sttId; + /** + * Preferred text-to-speech service id + */ + public @Nullable String ttsId; + /** + * Preferred voice id + */ + public @Nullable String voiceId; + /** + * List of interpreters + */ + public List hliIds = List.of(); + /** + * Dialog locale + */ + public @Nullable Locale locale; + /** + * Linked listening item + */ + public @Nullable String listeningItem; + /** + * Custom listening melody + */ + public @Nullable String listeningMelody; + /** + * True if an associated dialog is running + */ + public boolean running = false; + + public DialogRegistration(String sourceId, String sinkId) { + this.sourceId = sourceId; + this.sinkId = sinkId; + } +} diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java index 69860c8690f..7e792309ca4 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/VoiceManager.java @@ -284,6 +284,38 @@ void listenAndAnswer(@Nullable STTService stt, @Nullable TTSService tts, @Nullab */ void listenAndAnswer(DialogContext context) throws IllegalStateException; + /** + * Register a dialog, so it will be persisted and started any time the required services are available. + * + * Only one registration can be done for an audio source. + * + * @param registration with the desired services ids and options for the dialog + * + * @throws IllegalStateException if there is another registration for the same source + */ + void registerDialog(DialogRegistration registration) throws IllegalStateException; + + /** + * Removes a dialog registration and stops the associate dialog. + * + * @param registration with the desired services ids and options for the dialog + */ + void unregisterDialog(DialogRegistration registration); + + /** + * Removes a dialog registration and stops the associate dialog. + * + * @param sourceId the registration audio source id. + */ + void unregisterDialog(String sourceId); + + /** + * List current dialog registrations + * + * @return a list of {@link DialogRegistration} + */ + List getDialogRegistrations(); + /** * Retrieves a TTS service. * If a default name is configured and the service available, this is returned. Otherwise, the first available diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java index 2f1a7a81dbd..7fd5a8bfa24 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/DialogProcessor.java @@ -76,7 +76,7 @@ public class DialogProcessor implements KSListener, STTListener { private final Logger logger = LoggerFactory.getLogger(DialogProcessor.class); - private final DialogContext dialogContext; + public final DialogContext dialogContext; private @Nullable List listeningMelody; private final EventPublisher eventPublisher; private final TranslationProvider i18nProvider; @@ -220,6 +220,7 @@ public void stop() { closeStreamKS(); toggleProcessing(false); playStopSound(); + eventListener.onDialogStopped(dialogContext); } /** @@ -458,5 +459,12 @@ public interface DialogEventListener { * @param context used by the dialog processor */ void onBeforeDialogInterpretation(DialogContext context); + + /** + * Runs whenever the dialog it stopped + * + * @param context used by the dialog processor + */ + void onDialogStopped(DialogContext context); } } diff --git a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java index f4155febca2..311f19704c1 100644 --- a/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java +++ b/bundles/org.openhab.core.voice/src/main/java/org/openhab/core/voice/internal/VoiceManagerImpl.java @@ -28,6 +28,10 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import java.util.stream.Collectors; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -39,6 +43,7 @@ import org.openhab.core.audio.AudioStream; import org.openhab.core.audio.UnsupportedAudioFormatException; import org.openhab.core.audio.UnsupportedAudioStreamException; +import org.openhab.core.common.ThreadPoolManager; import org.openhab.core.config.core.ConfigOptionProvider; import org.openhab.core.config.core.ConfigurableService; import org.openhab.core.config.core.ParameterOption; @@ -46,7 +51,10 @@ import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.library.types.PercentType; +import org.openhab.core.storage.Storage; +import org.openhab.core.storage.StorageService; import org.openhab.core.voice.DialogContext; +import org.openhab.core.voice.DialogRegistration; import org.openhab.core.voice.KSService; import org.openhab.core.voice.STTService; import org.openhab.core.voice.TTSException; @@ -103,7 +111,8 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider, Dia private static final String CONFIG_PREFIX_DEFAULT_VOICE = "defaultVoice."; private final Logger logger = LoggerFactory.getLogger(VoiceManagerImpl.class); - + private final ScheduledExecutorService scheduledExecutorService = ThreadPoolManager + .getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON); // service maps private final Map ksServices = new HashMap<>(); private final Map sttServices = new HashMap<>(); @@ -114,6 +123,7 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider, Dia private final AudioManager audioManager; private final EventPublisher eventPublisher; private final TranslationProvider i18nProvider; + private final Storage dialogRegistrationStorage; private @Nullable Bundle bundle; @@ -129,18 +139,21 @@ public class VoiceManagerImpl implements VoiceManager, ConfigOptionProvider, Dia private @Nullable String defaultHLI; private @Nullable String defaultVoice; private final Map defaultVoices = new HashMap<>(); - - private Map dialogProcessors = new HashMap<>(); - private Map singleDialogProcessors = new ConcurrentHashMap<>(); - private @Nullable DialogContext lastDialogContext = null; + private final Map dialogProcessors = new HashMap<>(); + private final Map singleDialogProcessors = new ConcurrentHashMap<>(); + private @Nullable DialogContext lastDialogContext; + private @Nullable ScheduledFuture dialogRegistrationFuture; @Activate public VoiceManagerImpl(final @Reference LocaleProvider localeProvider, final @Reference AudioManager audioManager, - final @Reference EventPublisher eventPublisher, final @Reference TranslationProvider i18nProvider) { + final @Reference EventPublisher eventPublisher, final @Reference TranslationProvider i18nProvider, + final @Reference StorageService storageService) { this.localeProvider = localeProvider; this.audioManager = audioManager; this.eventPublisher = eventPublisher; this.i18nProvider = i18nProvider; + this.dialogRegistrationStorage = storageService.getStorage(DialogRegistration.class.getName(), + this.getClass().getClassLoader()); } @Activate @@ -151,7 +164,13 @@ protected void activate(BundleContext bundleContext, Map config) @Deactivate protected void deactivate() { - stopAllDialogs(); + dialogProcessors.values().forEach(DialogProcessor::stop); + dialogProcessors.clear(); + ScheduledFuture dialogRegistrationFuture = this.dialogRegistrationFuture; + if (dialogRegistrationFuture != null) { + dialogRegistrationFuture.cancel(true); + this.dialogRegistrationFuture = null; + } } @SuppressWarnings("null") @@ -336,8 +355,10 @@ public String interpret(String text, @Nullable String hliIdList) throws Interpre } } - private @Nullable Voice getVoice(String id) { - if (id.contains(":")) { + private @Nullable Voice getVoice(@Nullable String id) { + if (id == null) { + return null; + } else if (id.contains(":")) { // it is a fully qualified unique id String[] segments = id.split(":"); TTSService tts = getTTS(segments[0]); @@ -624,11 +645,6 @@ public void stopDialog(DialogContext context) throws IllegalStateException { stopDialog(context.source()); } - private void stopAllDialogs() { - dialogProcessors.values().forEach(DialogProcessor::stop); - dialogProcessors.clear(); - } - @Override @Deprecated public void listenAndAnswer() throws IllegalStateException { @@ -701,6 +717,49 @@ public void listenAndAnswer(DialogContext context) throws IllegalStateException } } + @Override + public void registerDialog(DialogRegistration registration) throws IllegalStateException { + if (dialogRegistrationStorage.containsKey(registration.sourceId)) { + throw new IllegalStateException(String.format( + "Cannot register dialog as a dialog is registered for audio source '%s'.", registration.sourceId)); + } + synchronized (dialogRegistrationStorage) { + dialogRegistrationStorage.put(registration.sourceId, registration); + } + scheduleDialogRegistrations(); + } + + @Override + public void unregisterDialog(DialogRegistration registration) { + unregisterDialog(registration.sourceId); + } + + @Override + public void unregisterDialog(String sourceId) { + synchronized (dialogRegistrationStorage) { + var registrationRef = dialogRegistrationStorage.remove(sourceId); + if (registrationRef != null) { + var dialog = dialogProcessors.get(sourceId); + if (dialog != null) { + stopDialog(dialog.dialogContext); + } + } + } + } + + @Override + public List getDialogRegistrations() { + var list = new ArrayList(); + dialogRegistrationStorage.getValues().forEach(dr -> { + if (dr != null) { + // update running state + dr.running = dialogProcessors.containsKey(dr.sourceId); + list.add(dr); + } + }); + return list; + } + private boolean checkLocales(Set supportedLocales, Locale locale) { if (supportedLocales.isEmpty()) { return true; @@ -712,40 +771,70 @@ private boolean checkLocales(Set supportedLocales, Locale locale) { }); } + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addAudioSink(AudioSink audioSink) { + scheduleDialogRegistrations(); + } + + protected void removeAudioSink(AudioSink audioSink) { + stopDialogs((dialog) -> dialog.dialogContext.sink().getId().equals(audioSink.getId())); + } + + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + protected void addAudioSource(AudioSource audioSource) { + scheduleDialogRegistrations(); + } + + protected void removeAudioSource(AudioSource audioSource) { + stopDialogs((dialog) -> dialog.dialogContext.source().getId().equals(audioSource.getId())); + } + @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addKSService(KSService ksService) { this.ksServices.put(ksService.getId(), ksService); + scheduleDialogRegistrations(); } protected void removeKSService(KSService ksService) { this.ksServices.remove(ksService.getId()); + stopDialogs((dialog) -> { + var ks = dialog.dialogContext.ks(); + return ks != null && ks.getId().equals(ksService.getId()); + }); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addSTTService(STTService sttService) { this.sttServices.put(sttService.getId(), sttService); + scheduleDialogRegistrations(); } protected void removeSTTService(STTService sttService) { this.sttServices.remove(sttService.getId()); + stopDialogs((dialog) -> dialog.dialogContext.stt().getId().equals(sttService.getId())); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addTTSService(TTSService ttsService) { this.ttsServices.put(ttsService.getId(), ttsService); + scheduleDialogRegistrations(); } protected void removeTTSService(TTSService ttsService) { this.ttsServices.remove(ttsService.getId()); + stopDialogs((dialog) -> dialog.dialogContext.tts().getId().equals(ttsService.getId())); } @Reference(cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) protected void addHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) { this.humanLanguageInterpreters.put(humanLanguageInterpreter.getId(), humanLanguageInterpreter); + scheduleDialogRegistrations(); } protected void removeHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) { this.humanLanguageInterpreters.remove(humanLanguageInterpreter.getId()); + stopDialogs((dialog) -> dialog.dialogContext.hlis().stream() + .anyMatch(hli -> hli.getId().equals(humanLanguageInterpreter.getId()))); } @Override @@ -765,8 +854,8 @@ protected void removeHumanLanguageInterpreter(HumanLanguageInterpreter humanLang } @Override - public @Nullable TTSService getTTS(String id) { - return ttsServices.get(id); + public @Nullable TTSService getTTS(@Nullable String id) { + return id == null ? null : ttsServices.get(id); } private @Nullable TTSService getTTS(Voice voice) { @@ -795,8 +884,8 @@ public Collection getTTSs() { } @Override - public @Nullable STTService getSTT(String id) { - return sttServices.get(id); + public @Nullable STTService getSTT(@Nullable String id) { + return id == null ? null : sttServices.get(id); } @Override @@ -821,8 +910,8 @@ public Collection getSTTs() { } @Override - public @Nullable KSService getKS(String id) { - return ksServices.get(id); + public @Nullable KSService getKS(@Nullable String id) { + return id == null ? null : ksServices.get(id); } @Override @@ -847,8 +936,8 @@ public Collection getKSs() { } @Override - public @Nullable HumanLanguageInterpreter getHLI(String id) { - return humanLanguageInterpreters.get(id); + public @Nullable HumanLanguageInterpreter getHLI(@Nullable String id) { + return id == null ? null : humanLanguageInterpreters.get(id); } @Override @@ -952,8 +1041,74 @@ private Comparator createVoiceComparator(Locale locale) { return null; } + private void stopDialogs(Predicate filter) { + synchronized (dialogRegistrationStorage) { + var dialogsToStop = dialogProcessors.values().stream().filter(filter).toList(); + if (dialogsToStop.isEmpty()) { + return; + } + for (var dialog : dialogsToStop) { + stopDialog(dialog.dialogContext.source()); + } + } + } + + /** + * In order to reduce the number of dialog registration builds + * this method schedules a call to {@link #buildDialogRegistrations() buildDialogRegistrations} in five seconds + * and cancel the previous scheduled call if any. + */ + private void scheduleDialogRegistrations() { + ScheduledFuture job = this.dialogRegistrationFuture; + if (job != null) { + job.cancel(false); + } + dialogRegistrationFuture = scheduledExecutorService.schedule(this::buildDialogRegistrations, 5, + TimeUnit.SECONDS); + } + + /** + * This method tries to start a dialog for each dialog registration. + * It's only called from {@link #scheduleDialogRegistrations() scheduleDialogRegistrations} in order to + * reduce the number of executions. + */ + private void buildDialogRegistrations() { + synchronized (dialogRegistrationStorage) { + dialogRegistrationStorage.getValues().stream().forEach(dr -> { + if (dr != null && !dialogProcessors.containsKey(dr.sourceId)) { + try { + startDialog(getDialogContextBuilder() // + .withSink(audioManager.getSink(dr.sinkId)) // + .withSource(audioManager.getSource(dr.sourceId)) // + .withKS(getKS(dr.ksId)) // + .withKeyword(dr.keyword) // + .withSTT(getSTT(dr.sttId)) // + .withTTS(getTTS(dr.ttsId)) // + .withVoice(getVoice(dr.voiceId)) // + .withHLIs(getHLIsByIds(dr.hliIds)) // + .withLocale(dr.locale) // + .withListeningItem(dr.listeningItem) // + .withMelody(dr.listeningMelody) // + .build()); + } catch (IllegalStateException e) { + logger.debug("Unable to start dialog registration: {}", e.getMessage()); + } + } + }); + } + } + @Override public void onBeforeDialogInterpretation(DialogContext context) { lastDialogContext = context; } + + @Override + public void onDialogStopped(DialogContext context) { + var registration = dialogRegistrationStorage.get(context.source().getId()); + if (registration != null) { + // try to rebuild in case it was manually stopped + scheduleDialogRegistrations(); + } + } } diff --git a/itests/org.openhab.core.voice.tests/itest.bndrun b/itests/org.openhab.core.voice.tests/itest.bndrun index c5d6aec2848..febdf64d8c6 100644 --- a/itests/org.openhab.core.voice.tests/itest.bndrun +++ b/itests/org.openhab.core.voice.tests/itest.bndrun @@ -63,6 +63,7 @@ Fragment-Host: org.openhab.core.voice biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.audio;version='[4.0.0,4.0.1)',\ + org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.io.http;version='[4.0.0,4.0.1)',\ diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java index f0076ee8a79..89ce42a9786 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/internal/VoiceManagerImplTest.java @@ -14,7 +14,11 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.net.URI; @@ -34,6 +38,7 @@ import org.openhab.core.i18n.LocaleProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.voice.DialogRegistration; import org.openhab.core.voice.Voice; import org.openhab.core.voice.VoiceManager; import org.openhab.core.voice.text.InterpretationException; @@ -74,16 +79,15 @@ public class VoiceManagerImplTest extends JavaOSGiTest { @BeforeEach public void setUp() throws IOException { + registerVolatileStorageService(); BundleContext context = bundleContext; ttsService = new TTSServiceStub(context); sink = new SinkStub(); voice = new VoiceStub(); source = new AudioSourceStub(); - registerService(sink); registerService(voice); registerService(source); - ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class); audioManager = getService(AudioManager.class); @@ -582,4 +586,48 @@ public void getPreferredVoiceOfEmptySet() { Voice voice = voiceManager.getPreferredVoice(Set.of()); assertNull(voice); } + + @Test + public void registerDialog() throws IOException, InterruptedException { + // register services + sttService = new STTServiceStub(); + ksService = new KSServiceStub(); + hliStub = new HumanLanguageInterpreterStub(); + registerService(sttService); + registerService(ksService); + registerService(ttsService); + registerService(hliStub); + // configure + Dictionary config = new Hashtable<>(); + config.put(CONFIG_KEYWORD, "word"); + config.put(CONFIG_DEFAULT_STT, sttService.getId()); + config.put(CONFIG_DEFAULT_KS, ksService.getId()); + config.put(CONFIG_DEFAULT_HLI, hliStub.getId()); + config.put(CONFIG_DEFAULT_VOICE, voice.getUID()); + ConfigurationAdmin configAdmin = super.getService(ConfigurationAdmin.class); + Configuration configuration = configAdmin.getConfiguration(VoiceManagerImpl.CONFIGURATION_PID); + configuration.update(config); + // Wait some time to be sure that the configuration will be updated + Thread.sleep(2000); + // Add a dialog registration + var dialogRegistration = new DialogRegistration(source.getId(), sink.getId()); + voiceManager.registerDialog(dialogRegistration); + // Wait some time to be sure dialog build has been fired and check dialog has been started + Thread.sleep(6000); + // Assert registration is available and running + var registrations = voiceManager.getDialogRegistrations(); + assertThat(registrations.size(), is(1)); + assertTrue(registrations.stream().findAny().map(r -> r.running).orElse(false)); + // Assert dialog has been stated + 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()); + // Remove the dialog registration + voiceManager.unregisterDialog(dialogRegistration); + // Assert dialog has been stopped + assertTrue(ksService.isAborted()); + } } diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java index 9c7c9282e39..8c5d674ed5b 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/InterpretCommandTest.java @@ -51,6 +51,7 @@ public class InterpretCommandTest extends VoiceConsoleCommandExtensionTest { @BeforeEach public void setUp() throws IOException, InterruptedException { + registerVolatileStorageService(); ttsService = new TTSServiceStub(); hliStub = new HumanLanguageInterpreterStub(); voice = new VoiceStub(); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/SayCommandTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/SayCommandTest.java index 25e3c940fc8..ea88352f35b 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/SayCommandTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/SayCommandTest.java @@ -61,6 +61,7 @@ public class SayCommandTest extends VoiceConsoleCommandExtensionTest { @BeforeEach public void setUp() { + registerVolatileStorageService(); sink = new SinkStub(); voice = new VoiceStub(); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoiceConsoleCommandExtensionTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoiceConsoleCommandExtensionTest.java index 230bca6fa56..6c54a858fe7 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoiceConsoleCommandExtensionTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoiceConsoleCommandExtensionTest.java @@ -44,6 +44,7 @@ public abstract class VoiceConsoleCommandExtensionTest extends JavaOSGiTest { @BeforeEach public void setup() { + registerVolatileStorageService(); voiceManager = getService(VoiceManager.class, VoiceManagerImpl.class); assertNotNull(voiceManager); audioManager = getService(AudioManager.class, AudioManager.class); diff --git a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java index 72b5fdea771..4559e4f4492 100644 --- a/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java +++ b/itests/org.openhab.core.voice.tests/src/main/java/org/openhab/core/voice/voiceconsolecommandextension/VoicesCommandTest.java @@ -50,6 +50,7 @@ public class VoicesCommandTest extends VoiceConsoleCommandExtensionTest { @BeforeEach public void setUp() throws IOException { + registerVolatileStorageService(); localeProvider = getService(LocaleProvider.class); assertNotNull(localeProvider);