diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath b/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath
new file mode 100644
index 00000000000..f725d321e1b
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/.classpath
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/.project b/bundles/core/org.eclipse.smarthome.core.voice.test/.project
new file mode 100644
index 00000000000..d17f40c1782
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/.project
@@ -0,0 +1,33 @@
+
+
+ org.eclipse.smarthome.core.voice.test
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.pde.ManifestBuilder
+
+
+
+
+ org.eclipse.pde.SchemaBuilder
+
+
+
+
+ org.eclipse.pde.ds.core.builder
+
+
+
+
+
+ org.eclipse.pde.PluginNature
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.jdt.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.jdt.core.prefs
similarity index 100%
rename from bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.jdt.core.prefs
rename to bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.jdt.core.prefs
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.pde.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.pde.core.prefs
similarity index 100%
rename from bundles/io/org.eclipse.smarthome.io.voice/.settings/org.eclipse.pde.core.prefs
rename to bundles/core/org.eclipse.smarthome.core.voice.test/.settings/org.eclipse.pde.core.prefs
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF
new file mode 100644
index 00000000000..3ad4b17d63b
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/META-INF/MANIFEST.MF
@@ -0,0 +1,12 @@
+Manifest-Version: 1.0
+Bundle-ManifestVersion: 2
+Bundle-Name: Eclipse SmartHome Core Voice Tests
+Bundle-SymbolicName: org.eclipse.smarthome.core.voice.test
+Bundle-Version: 0.9.0.qualifier
+Bundle-Vendor: Eclipse.org/SmartHome
+Fragment-Host: org.eclipse.smarthome.core.voice
+Bundle-RequiredExecutionEnvironment: JavaSE-1.7
+Import-Package: org.eclipse.smarthome.core.voice,
+ org.hamcrest.core,
+ org.junit;version="4.0.0"
+Bundle-ClassPath: .
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/about.html b/bundles/core/org.eclipse.smarthome.core.voice.test/about.html
similarity index 100%
rename from bundles/io/org.eclipse.smarthome.io.voice/about.html
rename to bundles/core/org.eclipse.smarthome.core.voice.test/about.html
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties b/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties
new file mode 100644
index 00000000000..df0687569fb
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/build.properties
@@ -0,0 +1,5 @@
+source.. = src/test/java/
+output.. = target/test-classes/
+bin.includes = META-INF/,\
+ .,\
+ about.html
diff --git a/bundles/io/org.eclipse.smarthome.io.audio/pom.xml b/bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml
similarity index 57%
rename from bundles/io/org.eclipse.smarthome.io.audio/pom.xml
rename to bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml
index 07e6a5410d7..9b7f9e9664c 100644
--- a/bundles/io/org.eclipse.smarthome.io.audio/pom.xml
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/pom.xml
@@ -3,21 +3,21 @@
org.eclipse.smarthome.bundles
- io
+ core0.9.0-SNAPSHOT
- org.eclipse.smarthome.io.audio
- org.eclipse.smarthome.io.audio
+ org.eclipse.smarthome.core.voice.test
+ org.eclipse.smarthome.core.voice.test4.0.0org.eclipse.smarthome.io
- org.eclipse.smarthome.io.audio
-
- Eclipse SmartHome Audio I/O
-
- eclipse-plugin
+ org.eclipse.smarthome.core.voice.test
+
+ Eclipse SmartHome Core Voice Test
+
+ eclipse-test-plugin
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java
new file mode 100644
index 00000000000..72eac03c0bb
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/STTExceptionTest.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test general purpose STT exception
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class STTExceptionTest {
+
+ /**
+ * Test STTException() constructor
+ */
+ @Test
+ public void testConstructor0() {
+ STTException ttsException = new STTException();
+ Assert.assertNotNull("STTException() constructor failed", ttsException);
+ }
+
+ /**
+ * Test STTException(String message, Throwable cause) constructor
+ */
+ @Test
+ public void testConstructor1() {
+ STTException ttsException = new STTException("Message", new Throwable());
+ Assert.assertNotNull("STTException(String, Throwable) constructor failed", ttsException);
+ }
+
+ /**
+ * Test STTException(String message) constructor
+ */
+ @Test
+ public void testConstructor2() {
+ STTException ttsException = new STTException("Message");
+ Assert.assertNotNull("STTException(String) constructor failed", ttsException);
+ }
+
+ /**
+ * Test STTException(Throwable cause) constructor
+ */
+ @Test
+ public void testConstructor3() {
+ STTException ttsException = new STTException(new Throwable());
+ Assert.assertNotNull("STTException(Throwable) constructor failed", ttsException);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java
new file mode 100644
index 00000000000..aa5d47da7ac
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEventTest.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test SpeechRecognitionErrorEvent event
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechRecognitionErrorEventTest {
+
+ /**
+ * Test SpeechRecognitionErrorEvent(String) constructor
+ */
+ @Test
+ public void testConstructor() {
+ SpeechRecognitionErrorEvent sRE = new SpeechRecognitionErrorEvent("Message");
+ Assert.assertNotNull("SpeechRecognitionErrorEvent(String) constructor failed", sRE);
+ }
+
+ /**
+ * Test SpeechRecognitionErrorEvent.getMessage() method
+ */
+ @Test
+ public void getMessageTest() {
+ SpeechRecognitionErrorEvent sRE = new SpeechRecognitionErrorEvent("Message");
+ Assert.assertEquals("SpeechRecognitionErrorEvent.getMessage() method failed", "Message", sRE.getMessage());
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java
new file mode 100644
index 00000000000..1d02583bce5
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEventTest.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test SpeechRecognitionEvent event
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechRecognitionEventTest {
+
+ /**
+ * Test SpeechRecognitionEvent(String, float) constructor
+ */
+ @Test
+ public void testConstructor() {
+ SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f);
+ Assert.assertNotNull("SpeechRecognitionEvent(String, float) constructor failed", sRE);
+ }
+
+ /**
+ * Test SpeechRecognitionEvent.getTranscript() method
+ */
+ @Test
+ public void getTranscriptTest() {
+ SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f);
+ Assert.assertEquals("SpeechRecognitionEvent.getTranscript() method failed", "Message", sRE.getTranscript());
+ }
+
+ /**
+ * Test SpeechRecognitionEvent.getConfidence() method
+ */
+ @Test
+ public void getConfidenceTest() {
+ SpeechRecognitionEvent sRE = new SpeechRecognitionEvent("Message", 0.5f);
+ Assert.assertEquals("SpeechRecognitionEvent.getConfidence() method failed", (double) 0.5f,
+ (double) sRE.getConfidence(), (double) 0.001f);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java
new file mode 100644
index 00000000000..aa89cc693ac
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/java/org/eclipse/smarthome/core/voice/TTSExceptionTest.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Test general purpose TTS exception
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class TTSExceptionTest {
+
+ /**
+ * Test TTSException() constructor
+ */
+ @Test
+ public void testConstructor0() {
+ TTSException ttsException = new TTSException();
+ Assert.assertNotNull("TTSException() constructor failed", ttsException);
+ }
+
+ /**
+ * Test TTSException(String message, Throwable cause) constructor
+ */
+ @Test
+ public void testConstructor1() {
+ TTSException ttsException = new TTSException("Message", new Throwable());
+ Assert.assertNotNull("TTSException(String, Throwable) constructor failed", ttsException);
+ }
+
+ /**
+ * Test TTSException(String message) constructor
+ */
+ @Test
+ public void testConstructor2() {
+ TTSException ttsException = new TTSException("Message");
+ Assert.assertNotNull("TTSException(String) constructor failed", ttsException);
+ }
+
+ /**
+ * Test TTSException(Throwable cause) constructor
+ */
+ @Test
+ public void testConstructor3() {
+ TTSException ttsException = new TTSException(new Throwable());
+ Assert.assertNotNull("TTSException(Throwable) constructor failed", ttsException);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw
new file mode 100644
index 00000000000..350b76a9290
Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/goforward.raw differ
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw
new file mode 100644
index 00000000000..1a1bff12867
Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/hellowworld.raw differ
diff --git a/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw
new file mode 100644
index 00000000000..9d9b5fa746a
Binary files /dev/null and b/bundles/core/org.eclipse.smarthome.core.voice.test/src/test/resources/org/eclipse/smarthome/io/voice/internal/marieta.raw differ
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.classpath b/bundles/core/org.eclipse.smarthome.core.voice/.classpath
similarity index 87%
rename from bundles/io/org.eclipse.smarthome.io.voice/.classpath
rename to bundles/core/org.eclipse.smarthome.core.voice/.classpath
index a95e0906ca0..19af0fa670a 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/.classpath
+++ b/bundles/core/org.eclipse.smarthome.core.voice/.classpath
@@ -3,5 +3,6 @@
+
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/.project b/bundles/core/org.eclipse.smarthome.core.voice/.project
similarity index 90%
rename from bundles/io/org.eclipse.smarthome.io.voice/.project
rename to bundles/core/org.eclipse.smarthome.core.voice/.project
index df455ff90f3..68ccbcf6203 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/.project
+++ b/bundles/core/org.eclipse.smarthome.core.voice/.project
@@ -1,6 +1,6 @@
- org.eclipse.smarthome.io.voice
+ org.eclipse.smarthome.core.voice
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 00000000000..f42de363afa
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
+org.eclipse.jdt.core.compiler.compliance=1.7
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.7
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs
new file mode 100644
index 00000000000..e67024ad352
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/.settings/org.eclipse.pde.core.prefs
@@ -0,0 +1,4 @@
+#Mon Oct 11 21:08:09 CEST 2010
+eclipse.preferences.version=1
+pluginProject.extensions=false
+resolve.requirebundle=false
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF b/bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF
similarity index 64%
rename from bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF
rename to bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF
index f22975677fe..07aa136700c 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/META-INF/MANIFEST.MF
+++ b/bundles/core/org.eclipse.smarthome.core.voice/META-INF/MANIFEST.MF
@@ -1,26 +1,27 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
-Bundle-Name: Eclipse SmartHome Voice I/O Bundle
-Bundle-SymbolicName: org.eclipse.smarthome.io.voice
+Bundle-Name: Eclipse SmartHome Core Voice
+Bundle-SymbolicName: org.eclipse.smarthome.core.voice
Bundle-Version: 0.9.0.qualifier
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-RequiredExecutionEnvironment: JavaSE-1.7
Bundle-ClassPath: .
-Import-Package: org.apache.commons.collections,
+Import-Package: org.apache.commons.collections.map,
org.apache.commons.io,
org.apache.commons.lang,
+ org.eclipse.smarthome.core.audio,
org.eclipse.smarthome.core.common.registry,
org.eclipse.smarthome.core.events,
+ org.eclipse.smarthome.core.i18n,
org.eclipse.smarthome.core.items,
org.eclipse.smarthome.core.items.events,
org.eclipse.smarthome.core.library.types,
org.eclipse.smarthome.core.types,
org.eclipse.smarthome.io.console,
org.eclipse.smarthome.io.console.extensions,
- org.eclipse.smarthome.io.voice.tts,
org.osgi.framework,
org.slf4j
-Export-Package: org.eclipse.smarthome.io.voice.text,
- org.eclipse.smarthome.io.voice.tts
+Export-Package: org.eclipse.smarthome.core.voice,
+ org.eclipse.smarthome.core.voice.text
Service-Component: OSGI-INF/*.xml
-Bundle-Activator: org.eclipse.smarthome.io.voice.internal.VoiceActivator
+Bundle-ActivationPolicy: lazy
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml
similarity index 75%
rename from bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml
rename to bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml
index 3d75b761775..221e8e07f61 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/StandardHumanLanguageInterpreter.xml
+++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/StandardInterpreter.xml
@@ -8,10 +8,10 @@
http://www.eclipse.org/legal/epl-v10.html
-->
-
-
+
+
-
+
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml
similarity index 68%
rename from bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml
rename to bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml
index 2cefef09349..7b6f1ff223a 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/OSGI-INF/SayConsoleCommandExtension.xml
+++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceConsoleCommandExtension.xml
@@ -8,10 +8,11 @@
http://www.eclipse.org/legal/epl-v10.html
-->
-
-
+
+
+
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml
new file mode 100644
index 00000000000..07a11ce2cd1
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/OSGI-INF/VoiceManager.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/about.html b/bundles/core/org.eclipse.smarthome.core.voice/about.html
new file mode 100644
index 00000000000..c258ef55d83
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/about.html
@@ -0,0 +1,28 @@
+
+
+
+
+About
+
+
+
About This Content
+
+
June 5, 2006
+
License
+
+
The Eclipse Foundation makes available all content in this plug-in ("Content"). Unless otherwise
+indicated below, the Content is provided to you under the terms and conditions of the
+Eclipse Public License Version 1.0 ("EPL"). A copy of the EPL is available
+at http://www.eclipse.org/legal/epl-v10.html.
+For purposes of the EPL, "Program" will mean the Content.
+
+
If you did not receive this Content directly from the Eclipse Foundation, the Content is
+being redistributed by another party ("Redistributor") and different terms and conditions may
+apply to your use of any object code in the Content. Check the Redistributor's license that was
+provided with the Content. If no such license exists, contact the Redistributor. Unless otherwise
+indicated below, the terms and conditions of the EPL still apply to any source code in the Content
+and such source code may be obtained at http://www.eclipse.org.
+
+
+
\ No newline at end of file
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/build.properties b/bundles/core/org.eclipse.smarthome.core.voice/build.properties
new file mode 100644
index 00000000000..9e97e61f6a9
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/build.properties
@@ -0,0 +1,7 @@
+output.. = target/classes/
+bin.includes = META-INF/,\
+ .,\
+ OSGI-INF/,\
+ about.html
+source.. = src/main/java/,\
+ src/main/resources/
diff --git a/bundles/io/org.eclipse.smarthome.io.voice/pom.xml b/bundles/core/org.eclipse.smarthome.core.voice/pom.xml
similarity index 58%
rename from bundles/io/org.eclipse.smarthome.io.voice/pom.xml
rename to bundles/core/org.eclipse.smarthome.core.voice/pom.xml
index 677bee96504..1a69602f139 100644
--- a/bundles/io/org.eclipse.smarthome.io.voice/pom.xml
+++ b/bundles/core/org.eclipse.smarthome.core.voice/pom.xml
@@ -3,20 +3,21 @@
org.eclipse.smarthome.bundles
- io
+ core0.9.0-SNAPSHOT
- org.eclipse.smarthome.io.voice
- org.eclipse.smarthome.io.voice
+ org.eclipse.smarthome.core.voice
+ org.eclipse.smarthome.core.voice4.0.0
- org.eclipse.smarthome.io
- org.eclipse.smarthome.io.voice
+ org.eclipse.smarthome.core
+ org.eclipse.smarthome.core.voice
- Eclipse SmartHome Voice I/O
+ Eclipse SmartHome Core Voiceeclipse-plugin
+
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java
new file mode 100644
index 00000000000..73186e1a2af
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStartEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} starts hearing audio.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class AudioStartEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java
new file mode 100644
index 00000000000..ae28a187f80
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/AudioStopEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} stops hearing audio.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class AudioStopEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java
new file mode 100644
index 00000000000..d6520398f8d
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSErrorEvent.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link KSEvent} fired when the {@link KSService} encounters an error.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class KSErrorEvent implements KSEvent {
+ /**
+ * The message describing the error
+ */
+ private final String message;
+
+ /**
+ * Constructs an instance with the passed {@code message}.
+ *
+ * @param message The message describing the error
+ */
+ public KSErrorEvent(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Gets the message describing this error
+ *
+ * @return The message describing this error
+ */
+ public String getMessage() {
+ return this.message;
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java
new file mode 100644
index 00000000000..15c0f0dd2d3
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A tagging interface for keyword spotting events.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface KSEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java
new file mode 100644
index 00000000000..2d7ec731916
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSException.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * General purpose keyword spotting exception
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class KSException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public KSException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message Detail message
+ * @param cause The cause
+ */
+ public KSException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public KSException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public KSException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java
new file mode 100644
index 00000000000..2c439293db7
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSListener.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * The listener interface for receiving {@link KSEvent} events.
+ *
+ * A class interested in processing {@link KSEvent} events implements this interface,
+ * and its instances are passed to the {@code KSService}'s {@code spot()} method.
+ * Such instances are then targeted for various {@link KSEvent} events corresponding
+ * to the keyword spotting process.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface KSListener {
+ /**
+ * Invoked when a {@link KSEvent} event occurs during keyword spotting.
+ *
+ * @param ksEvent The {@link KSEvent} fired by the {@link KSService}
+ */
+ public void ksEventReceived(KSEvent ksEvent);
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java
new file mode 100644
index 00000000000..6fa75d0965a
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSService.java
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import java.util.Locale;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.audio.AudioFormat;
+import org.eclipse.smarthome.core.audio.AudioStream;
+
+/**
+ * This is the interface that a keyword spotting service has to implement.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ * @author Kai Kreuzer - Refactored to use AudioStream
+ */
+public interface KSService {
+
+ /**
+ * Returns a simple string that uniquely identifies this service
+ *
+ * @return an id that identifies this service
+ */
+ public String getId();
+
+ /**
+ * Returns a localized human readable label that can be used within UIs.
+ *
+ * @param locale the locale to provide the label for
+ * @return a localized string to be used in UIs
+ */
+ public String getLabel(Locale locale);
+
+ /**
+ * Obtain the Locales available from this KSService
+ *
+ * @return The Locales available from this service
+ */
+ public Set getSupportedLocales();
+
+ /**
+ * Obtain the audio formats supported by this KSService
+ *
+ * @return The audio formats supported by this service
+ */
+ public Set getSupportedFormats();
+
+ /**
+ * This method starts the process of keyword spotting
+ *
+ * The audio data of the passed {@link AudioStream} is passed to the keyword
+ * spotting engine. The keyword spotting attempts to spot {@code keyword} as
+ * being spoken in the passed {@code Locale}. Spotted keyword is indicated by
+ * fired {@link KSEvent} events targeting the passed {@link KSListener}.
+ *
+ * The passed {@link AudioStream} must be of a supported {@link AudioFormat}.
+ * In other words a {@link AudioFormat} compatible with one returned from
+ * the {@code getSupportedFormats()} method.
+ *
+ * The passed {@code Locale} must be supported. That is to say it must be
+ * a {@code Locale} returned from the {@code getSupportedLocales()} method.
+ *
+ * The passed {@code keyword} is the keyword which should be spotted.
+ *
+ * The method is supposed to return fast, i.e. it should only start the spotting as a background process.
+ *
+ * @param ksListener Non-null {@link KSListener} that {@link KSEvent} events target
+ * @param audioStream The {@link AudioStream} from which keywords are spotted
+ * @param locale The {@code Locale} in which the target keywords are spoken
+ * @param keyword The keyword which to spot
+ * @return A {@link KSServiceHandle} used to abort keyword spotting
+ * @throws A {@link KSException} if any parameter is invalid or a problem occurs
+ */
+ public KSServiceHandle spot(KSListener ksListener, AudioStream audioStream, Locale locale, String keyword)
+ throws KSException;
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java
new file mode 100644
index 00000000000..a9936ea931e
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSServiceHandle.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * An handle to a {@link KSService}
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface KSServiceHandle {
+ /**
+ * Aborts keyword spotting in the associated {@link KSService}
+ */
+ public void abort();
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java
new file mode 100644
index 00000000000..76565aaf265
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/KSpottedEvent.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import org.eclipse.smarthome.core.audio.AudioSource;
+
+/**
+ * A {@link KSEvent} fired when the {@link KSService} spots a keyword.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class KSpottedEvent implements KSEvent {
+ /**
+ * AudioSource from which the keyword was spotted
+ */
+ private final AudioSource audioSource;
+
+ /**
+ * Constructs an instance with the passed {@code audioSource}
+ *
+ * @param audioSource The AudioSource of the spotted keyword
+ */
+ public KSpottedEvent(AudioSource audioSource) {
+ if (null == audioSource) {
+ throw new IllegalArgumentException("The passed audioSource is null");
+ }
+
+ this.audioSource = audioSource;
+ }
+
+ /**
+ * Returns the audioSource of the spotted keyword
+ *
+ * @return The audioSource of the spotted keyword
+ */
+ public AudioSource getAudioSource() {
+ return this.audioSource;
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java
new file mode 100644
index 00000000000..77c6b4ebeef
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStartEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} starts recognition.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class RecognitionStartEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java
new file mode 100644
index 00000000000..b40f23e332d
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/RecognitionStopEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} stops recognition.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class RecognitionStopEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java
new file mode 100644
index 00000000000..d7acc06cb12
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A tagging interface for speech-to-text events.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java
new file mode 100644
index 00000000000..8b4aa95ad35
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTException.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * General purpose STT exception
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class STTException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public STTException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message Detail message
+ * @param cause The cause
+ */
+ public STTException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public STTException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public STTException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java
new file mode 100644
index 00000000000..92a0ac0462d
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTListener.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * The listener interface for receiving {@link STTEvent} events.
+ *
+ * A class interested in processing {@link STTEvent} events implements this interface,
+ * and its instances are passed to the {@code STTService}'s {@code recognize()} method.
+ * Such instances are then targeted for various {@link STTEvent} events corresponding
+ * to the speech recognition process.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface STTListener {
+
+ /**
+ * Invoked when a {@link STTEvent} event occurs during speech recognition.
+ *
+ * @param sttEvent The {@link STTEvent} fired by the {@link STTService}
+ */
+ public void sttEventReceived(STTEvent sttEvent);
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java
new file mode 100644
index 00000000000..f871b6ad7fb
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTService.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import java.util.Locale;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.audio.AudioFormat;
+import org.eclipse.smarthome.core.audio.AudioSource;
+import org.eclipse.smarthome.core.audio.AudioStream;
+
+/**
+ * This is the interface that a speech-to-text service has to implement.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface STTService {
+
+ /**
+ * Returns a simple string that uniquely identifies this service
+ *
+ * @return an id that identifies this service
+ */
+ public String getId();
+
+ /**
+ * Returns a localized human readable label that can be used within UIs.
+ *
+ * @param locale the locale to provide the label for
+ * @return a localized string to be used in UIs
+ */
+ public String getLabel(Locale locale);
+
+ /**
+ * Obtain the Locales available from this STTService
+ *
+ * @return The Locales available from this service
+ */
+ public Set getSupportedLocales();
+
+ /**
+ * Obtain the audio formats supported by this STTService
+ *
+ * @return The audio formats supported by this service
+ */
+ public Set getSupportedFormats();
+
+ /**
+ * This method starts the process of speech recognition.
+ *
+ * The audio data of the passed {@link AudioSource} is passed to the speech
+ * recognition engine. The recognition engine attempts to recognize speech
+ * as being spoken in the passed {@code Locale} and containing statements
+ * specified in the passed {@code grammars}. Recognition is indicated by
+ * fired {@link STTEvent} events targeting the passed {@link STTListener}.
+ *
+ * The passed {@link AudioSource} must be of a supported {@link AudioFormat}.
+ * In other words a {@link AudioFormat} compatible with one returned from
+ * the {@code getSupportedFormats()} method.
+ *
+ * The passed {@code Locale} must be supported. That is to say it must be
+ * a {@code Locale} returned from the {@code getSupportedLocales()} method.
+ *
+ * The passed {@code grammars} must consist of a syntactically valid grammar
+ * as specified by the JSpeech Grammar Format. If {@code grammars} is null
+ * or empty, large vocabulary continuous speech recognition is attempted.
+ *
+ * @see JSpeech Grammar Format.
+ * @param sttListener Non-null {@link STTListener} that {@link STTEvent} events target
+ * @param audioStream The {@link AudioStream} from which speech is recognized
+ * @param locale The {@code Locale} in which the target speech is spoken
+ * @param grammars The JSpeech Grammar Format grammar specifying allowed statements
+ * @return A {@link STTServiceHandle} used to abort recognition
+ * @throws A {@link SSTException} if any parameter is invalid or a STT problem occurs
+ */
+ public STTServiceHandle recognize(STTListener sttListener, AudioStream audioStream, Locale locale,
+ Set grammars) throws STTException;
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java
new file mode 100644
index 00000000000..a1db2ae745c
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/STTServiceHandle.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * An handle to a {@link STTService}
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface STTServiceHandle {
+ /**
+ * Aborts recognition in the associated {@link STTService}
+ */
+ public void abort();
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java
new file mode 100644
index 00000000000..9be554292ce
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionErrorEvent.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} encounters an error.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechRecognitionErrorEvent implements STTEvent {
+ /**
+ * The message describing the error
+ */
+ private final String message;
+
+ /**
+ * Constructs an instance with the passed {@code message}.
+ *
+ * @param message The message describing the error
+ */
+ public SpeechRecognitionErrorEvent(String message) {
+ this.message = message;
+ }
+
+ /**
+ * Gets the message describing this error
+ *
+ * @return The message describing this error
+ */
+ public String getMessage() {
+ return this.message;
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java
new file mode 100644
index 00000000000..c14d6c0714b
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechRecognitionEvent.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} recognizes speech.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechRecognitionEvent implements STTEvent {
+ /**
+ * Confidence of recognized speech
+ */
+ private final float confidence;
+
+ /**
+ * Transcript of recognized speech
+ */
+ private final String transcript;
+
+ /**
+ * Constructs an instance with the passed {@code transcript} and {@code confidence}.
+ *
+ * The confidence represents a numeric estimate between 0 and 1, inclusively, of how
+ * confident the recognition engine is of the transcript. A higher number means the
+ * system is more confident.
+ *
+ * @param transcript The transcript of the recognized speech
+ * @param confidence The confidence of the transcript
+ */
+ public SpeechRecognitionEvent(String transcript, float confidence) {
+ if ((null == transcript) || (transcript.isEmpty())) {
+ throw new IllegalArgumentException("The passed transcript is null or empty");
+ }
+ if ((confidence < 0.0) || (1.0 < confidence)) {
+ throw new IllegalArgumentException("The passed confidence is less than 0.0 or greater than 1.0");
+ }
+
+ this.transcript = transcript;
+ this.confidence = confidence;
+ }
+
+ /**
+ * Returns the transcript of the recognized speech.
+ *
+ * @return The transcript of the recognized speech.
+ */
+ public String getTranscript() {
+ return this.transcript;
+ }
+
+ /**
+ * Returns the confidence of the transcript.
+ *
+ * The confidence represents a numeric estimate between 0 and 1, inclusively, of how
+ * confident the recognition engine is of the transcript. A higher number means the
+ * system is more confident.
+ *
+ * @return The transcript of the recognized speech.
+ */
+ public float getConfidence() {
+ return this.confidence;
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java
new file mode 100644
index 00000000000..63f0077b07d
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStartEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} starts hearing speech.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechStartEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java
new file mode 100644
index 00000000000..c27acefde3c
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/SpeechStopEvent.java
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * A {@link STTEvent} fired when the {@link STTService} stops hearing speech.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class SpeechStopEvent implements STTEvent {
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java
new file mode 100644
index 00000000000..62ea9369e11
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSException.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+/**
+ * General purpose TTS exception
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public class TTSException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Constructs a new exception with null as its detail message.
+ */
+ public TTSException() {
+ super();
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message and cause.
+ *
+ * @param message Detail message
+ * @param cause The cause
+ */
+ public TTSException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Constructs a new exception with the specified detail message.
+ *
+ * @param message Detail message
+ */
+ public TTSException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new exception with the specified cause.
+ *
+ * @param cause The cause
+ */
+ public TTSException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java
new file mode 100644
index 00000000000..db81882a0c4
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/TTSService.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import java.util.Locale;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.audio.AudioFormat;
+import org.eclipse.smarthome.core.audio.AudioStream;
+
+/**
+ * This is the interface that a text-to-speech service has to implement.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ * @author Kai Kreuzer - Refactored to use AudioStreams
+ */
+public interface TTSService {
+
+ /**
+ * Returns a simple string that uniquely identifies this service
+ *
+ * @return an id that identifies this service
+ */
+ public String getId();
+
+ /**
+ * Returns a localized human readable label that can be used within UIs.
+ *
+ * @param locale the locale to provide the label for
+ * @return a localized string to be used in UIs
+ */
+ public String getLabel(Locale locale);
+
+ /**
+ * Obtain the voices available from this TTSService
+ *
+ * @return The voices available from this service
+ */
+ public Set getAvailableVoices();
+
+ /**
+ * Obtain the audio formats supported by this TTSService
+ *
+ * @return The audio formats supported by this service
+ */
+ public Set getSupportedFormats();
+
+ /**
+ * Returns an {@link AudioStream} containing the TTS results. Note, one
+ * can only request a supported {@code Voice} and {@link AudioStream} or
+ * an exception is thrown.
+ *
+ * @param text The text to convert to speech
+ * @param voice The voice to use for speech
+ * @param requestedFormat The audio format to return the results in
+ * @return AudioStream containing the TTS results
+ * @throws TTSException If {@code voice} and/or {@code requestedFormat}
+ * are not supported or another error occurs while creating an
+ * {@link AudioStream}
+ */
+ public AudioStream synthesize(String text, Voice voice, AudioFormat requestedFormat) throws TTSException;
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java
new file mode 100644
index 00000000000..17b11f5db4b
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/Voice.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import java.util.Locale;
+
+/**
+ * This is the interface that a text-to-speech voice has to implement.
+ *
+ * @author Kelly Davis - Initial contribution and API
+ */
+public interface Voice {
+
+ /**
+ * Globally unique identifier of the voice, must have the format
+ * "prefix:voicename", where "prefix" is the id of the related TTS service.
+ *
+ * @return A String uniquely identifying the voice.
+ */
+ public String getUID();
+
+ /**
+ * The voice label, usually used for GUIs
+ *
+ * @return The voice label, may not be globally unique
+ */
+ public String getLabel();
+
+ /**
+ * Locale of the voice
+ *
+ * @return Locale of the voice
+ */
+ public Locale getLocale();
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java
new file mode 100644
index 00000000000..aee32542181
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/VoiceManager.java
@@ -0,0 +1,599 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.audio.AudioFormat;
+import org.eclipse.smarthome.core.audio.AudioSink;
+import org.eclipse.smarthome.core.audio.AudioSource;
+import org.eclipse.smarthome.core.audio.AudioStream;
+import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException;
+import org.eclipse.smarthome.core.i18n.LocaleProvider;
+import org.eclipse.smarthome.core.voice.internal.DialogProcessor;
+import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This service provides functionality around voice services and is the central service to be used directly by others.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public class VoiceManager {
+
+ // the default keyword to use if no other is configured
+ private static final String DEFAULT_KEYWORD = "Wakeup";
+
+ // constants for the configuration properties
+ private static final String CONFIG_KEYWORD = "keyword";
+ 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_TTS = "defaultTTS";
+ private static final String CONFIG_DEFAULT_SINK = "defaultSink";
+ private static final String CONFIG_DEFAULT_SOURCE = "defaultSource";
+ private static final String CONFIG_PREFIX_DEFAULT_VOICE = "defaultVoice.";
+
+ private final Logger logger = LoggerFactory.getLogger(VoiceManager.class);
+
+ // service maps
+ private Map ksServices = new HashMap<>();
+ private Map sttServices = new HashMap<>();
+ private Map ttsServices = new HashMap<>();
+ private Map humanLanguageInterpreters = new HashMap<>();
+ private Map audioSources = new HashMap<>();
+ private Map audioSinks = new HashMap<>();
+
+ private LocaleProvider localeProvider = null;
+
+ /**
+ * default settings filled through the service configuration
+ */
+ private String keyword = DEFAULT_KEYWORD;
+ private String defaultSource = null;
+ private String defaultSink = null;
+ private String defaultTTS = null;
+ private String defaultSTT = null;
+ private String defaultKS = null;
+ private String defaultHLI = null;
+ private Map defaultVoices = new HashMap<>();
+
+ protected void activate(Map config) {
+ modified(config);
+ }
+
+ protected void deactivate() {
+ }
+
+ protected void modified(Map config) {
+ if (config != null) {
+ this.keyword = config.containsKey(CONFIG_KEYWORD) ? config.get(CONFIG_KEYWORD).toString() : DEFAULT_KEYWORD;
+ this.defaultSource = config.containsKey(CONFIG_DEFAULT_SOURCE)
+ ? config.get(CONFIG_DEFAULT_SOURCE).toString() : null;
+ this.defaultSink = config.containsKey(CONFIG_DEFAULT_SINK) ? config.get(CONFIG_DEFAULT_SINK).toString()
+ : null;
+ this.defaultTTS = config.containsKey(CONFIG_DEFAULT_TTS) ? config.get(CONFIG_DEFAULT_TTS).toString() : null;
+ this.defaultSTT = config.containsKey(CONFIG_DEFAULT_STT) ? config.get(CONFIG_DEFAULT_STT).toString() : null;
+ this.defaultKS = config.containsKey(CONFIG_DEFAULT_KS) ? config.get(CONFIG_DEFAULT_KS).toString() : null;
+ this.defaultHLI = config.containsKey(CONFIG_DEFAULT_HLI) ? config.get(CONFIG_DEFAULT_HLI).toString() : null;
+
+ for (String key : config.keySet()) {
+ if (key.startsWith(CONFIG_PREFIX_DEFAULT_VOICE)) {
+ String tts = key.substring(CONFIG_PREFIX_DEFAULT_VOICE.length());
+ defaultVoices.put(tts, config.get(key).toString());
+ }
+ }
+ }
+ }
+
+ /**
+ * Speaks the passed string using the default TTS service and default audio sink.
+ *
+ * @param text The text to say
+ */
+ public void say(String text) {
+ say(text, null);
+ }
+
+ /**
+ * Speaks the passed string using the provided voiceId and the default audio sink.
+ * If the voiceId is fully qualified (i.e. with a tts prefix), the according TTS service will be used, otherwise the
+ * voiceId is assumed to be available on the default TTS service.
+ *
+ * @param text The text to say
+ * @param voiceId The id of the voice to use (either with or without prefix)
+ */
+ public void say(String text, String voiceId) {
+ say(text, voiceId, null);
+ }
+
+ /**
+ * Speaks the passed string using the provided voiceId and the given audio sink.
+ * If the voiceId is fully qualified (i.e. with a tts prefix), the according TTS service will be used, otherwise the
+ * voiceId is assumed to be available on the default TTS service.
+ *
+ * @param text The text to say
+ * @param voiceId The id of the voice to use (either with or without prefix) or null
+ * @param sinkId The id of the audio sink to use or null
+ */
+ public void say(String text, String voiceId, String sinkId) {
+ try {
+ TTSService tts = null;
+ Voice voice = null;
+ if (voiceId == null) {
+ tts = getTTS();
+ voice = getPreferredVoice(tts.getAvailableVoices());
+ } else if (voiceId.contains(":")) {
+ // it is a fully qualified unique id
+ String[] segments = voiceId.split(":");
+ tts = ttsServices.get(segments[0]);
+ voice = getVoice(tts.getAvailableVoices(), segments[1]);
+ } else {
+ // voiceId is not fully qualified
+ tts = getTTS();
+ voice = getVoice(tts.getAvailableVoices(), voiceId);
+ }
+ if (null == voice) {
+ throw new TTSException(
+ "Unable to find a voice for language " + localeProvider.getLocale().getLanguage());
+ }
+ Set audioFormats = tts.getSupportedFormats();
+ AudioSink sink = null;
+ if (sinkId == null) {
+ sink = getSink();
+ } else {
+ sink = audioSinks.get(sinkId);
+ }
+ if (sink != null) {
+ AudioFormat audioFormat = getBestMatch(audioFormats, sink.getSupportedFormats());
+ if (audioFormat != null) {
+ AudioStream audioStream = tts.synthesize(text, voice, audioFormat);
+
+ try {
+ sink.process(audioStream);
+ } catch (UnsupportedAudioFormatException e) {
+ logger.error("Error saying '{}': {}", text, e.getMessage());
+ }
+ } else {
+ logger.warn("No compatible audio format found for TTS '{}' and sink '{}'", tts.getId(),
+ sink.getId());
+ }
+ }
+ } catch (TTSException e) {
+ logger.error("Error saying '{}'", text);
+ }
+ }
+
+ private Voice getVoice(Set voices, String id) {
+ for (Voice voice : voices) {
+ if (voice.getUID().endsWith(":" + id)) {
+ return voice;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the first concrete AudioFormat in the passed set or a preferred one
+ * based on 16bit, 16KHz, big endian default
+ *
+ * @param audioFormats The AudioFormats from which to choose
+ * @return The preferred AudioFormat. A passed concrete format is preferred adding
+ * default values to an abstract AudioFormat in the passed Set.
+ */
+ public static AudioFormat getPreferredFormat(Set audioFormats) {
+ // Return the first concrete AudioFormat found
+ for (AudioFormat currentAudioFormat : audioFormats) {
+ // Check if currentAudioFormat is abstract
+ if (null == currentAudioFormat.getCodec()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getContainer()) {
+ continue;
+ }
+ if (null == currentAudioFormat.isBigEndian()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getBitDepth()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getBitRate()) {
+ continue;
+ }
+ if (null == currentAudioFormat.getFrequency()) {
+ continue;
+ }
+
+ // Prefer WAVE container
+ if (!currentAudioFormat.getContainer().equals("WAVE")) {
+ continue;
+ }
+
+ // As currentAudioFormat is concrete, use it
+ return currentAudioFormat;
+ }
+
+ // There's no concrete AudioFormat so we must create one
+ for (AudioFormat currentAudioFormat : audioFormats) {
+ // Define AudioFormat to return
+ AudioFormat format = currentAudioFormat;
+
+ // Not all Codecs and containers can be supported
+ if (null == format.getCodec()) {
+ continue;
+ }
+ if (null == format.getContainer()) {
+ continue;
+ }
+
+ // Prefer WAVE container
+ if (!format.getContainer().equals(AudioFormat.CONTAINER_WAVE)) {
+ continue;
+ }
+
+ // If required set BigEndian, BitDepth, BitRate, and Frequency to default values
+ if (null == format.isBigEndian()) {
+ format = new AudioFormat(format.getContainer(), format.getCodec(), new Boolean(true),
+ format.getBitDepth(), format.getBitRate(), format.getFrequency());
+ }
+ if (null == format.getBitDepth() || null == format.getBitRate() || null == format.getFrequency()) {
+ // Define default values
+ int defaultBitDepth = 16;
+ long defaultFrequency = 16384;
+
+ // Obtain current values
+ Integer bitRate = format.getBitRate();
+ Long frequency = format.getFrequency();
+ Integer bitDepth = format.getBitDepth();
+
+ // These values must be interdependent (bitRate = bitDepth * frequency)
+ if (null == bitRate) {
+ if (null == bitDepth) {
+ bitDepth = new Integer(defaultBitDepth);
+ }
+ if (null == frequency) {
+ frequency = new Long(defaultFrequency);
+ }
+ bitRate = new Integer(bitDepth.intValue() * frequency.intValue());
+ } else if (null == bitDepth) {
+ if (null == frequency) {
+ frequency = new Long(defaultFrequency);
+ }
+ bitDepth = new Integer(bitRate.intValue() / frequency.intValue());
+ } else if (null == frequency) {
+ frequency = new Long(bitRate.longValue() / bitDepth.longValue());
+ }
+
+ format = new AudioFormat(format.getContainer(), format.getCodec(), format.isBigEndian(), bitDepth,
+ bitRate, frequency);
+ }
+
+ // Return preferred AudioFormat
+ return format;
+ }
+
+ // Return null indicating failure
+ return null;
+ }
+
+ /**
+ * Determines the best match between a list of audio formats supported by a source and a sink.
+ *
+ * @param inputs the supported audio formats of an audio source
+ * @param outputs the supported audio formats of an audio sink
+ * @return the best matching format or null, if source and sink are incompatible
+ */
+ public static AudioFormat getBestMatch(Set inputs, Set outputs) {
+ AudioFormat preferredFormat = getPreferredFormat(inputs);
+ for (AudioFormat output : outputs) {
+ if (output.isCompatible(preferredFormat)) {
+ return preferredFormat;
+ } else {
+ for (AudioFormat input : inputs) {
+ if (output.isCompatible(input)) {
+ return input;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Determines the preferred voice for the currently set locale
+ *
+ * @param voices a set of voices to chose from
+ * @return the preferred voice for the current locale
+ */
+ public Voice getPreferredVoice(Set voices) {
+ // Express preferences with a Language Priority List
+ Locale locale = localeProvider.getLocale();
+
+ // Get collection of voice locales
+ Collection locales = new ArrayList();
+ for (Voice currentVoice : voices) {
+ locales.add(currentVoice.getLocale());
+ }
+
+ // TODO: This can be activated for Java 8
+ // Determine preferred locale based on RFC 4647
+ // String ranges = locale.toLanguageTag();
+ // List languageRanges = Locale.LanguageRange.parse(ranges);
+ // Locale preferedLocale = Locale.lookup(languageRanges,locales);
+ Locale preferredLocale = locale;
+
+ // As a last resort choose some Locale
+ if (null == preferredLocale) {
+ preferredLocale = locales.iterator().next();
+ }
+
+ // Determine preferred voice
+ Voice preferredVoice = null;
+ for (Voice currentVoice : voices) {
+ if (preferredLocale.equals(currentVoice.getLocale())) {
+ preferredVoice = currentVoice;
+ }
+ }
+ assert (preferredVoice != null);
+
+ // Return preferred voice
+ return preferredVoice;
+ }
+
+ /**
+ * Starts listening for the keyword that starts a dialog
+ *
+ * @throws IllegalStateException if required services are not available
+ */
+ public void startDialog() {
+ startDialog(null, null, null, null, null, null, null, this.keyword);
+ }
+
+ /**
+ * Starts listening for the keyword that starts a dialog
+ *
+ * @throws IllegalStateException if required services are not available
+ */
+ public void startDialog(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli,
+ AudioSource source, AudioSink sink, Locale locale, String keyword) {
+
+ // use defaults, if null
+ ks = (ks == null) ? getKS() : ks;
+ stt = (stt == null) ? getSTT() : stt;
+ tts = (tts == null) ? getTTS() : tts;
+ hli = (hli == null) ? getHLI() : hli;
+ source = (source == null) ? getSource() : source;
+ sink = (sink == null) ? getSink() : sink;
+ locale = (locale == null) ? localeProvider.getLocale() : locale;
+
+ if (ks != null && stt != null && tts != null && hli != null && source != null && sink != null) {
+ DialogProcessor processor = new DialogProcessor(ks, stt, tts, hli, source, sink, localeProvider.getLocale(),
+ keyword);
+ processor.start();
+ } else {
+ String msg = "Cannot start dialog as services are missing.";
+ logger.error(msg);
+ throw new IllegalStateException(msg);
+ }
+ }
+
+ protected void setLocaleProvider(LocaleProvider localeProvider) {
+ this.localeProvider = localeProvider;
+ }
+
+ protected void unsetLocaleProvider(LocaleProvider localeProvider) {
+ this.localeProvider = null;
+ }
+
+ protected void addKSService(KSService ksService) {
+ this.ksServices.put(ksService.getId(), ksService);
+ }
+
+ protected void removeKSService(KSService ksService) {
+ this.ksServices.remove(ksService.getId());
+ }
+
+ protected void addSTTService(STTService sttService) {
+ this.sttServices.put(sttService.getId(), sttService);
+ }
+
+ protected void removeSTTService(STTService sttService) {
+ this.sttServices.remove(sttService.getId());
+ }
+
+ protected void addTTSService(TTSService ttsService) {
+ this.ttsServices.put(ttsService.getId(), ttsService);
+ }
+
+ protected void removeTTSService(TTSService ttsService) {
+ this.ttsServices.remove(ttsService.getId());
+ }
+
+ protected void addHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) {
+ this.humanLanguageInterpreters.put(humanLanguageInterpreter.getId(), humanLanguageInterpreter);
+ }
+
+ protected void removeHumanLanguageInterpreter(HumanLanguageInterpreter humanLanguageInterpreter) {
+ this.humanLanguageInterpreters.remove(humanLanguageInterpreter.getId());
+ }
+
+ protected void addAudioSource(AudioSource audioSource) {
+ this.audioSources.put(audioSource.getId(), audioSource);
+ }
+
+ protected void removeAudioSource(AudioSource audioSource) {
+ this.audioSources.remove(audioSource.getId());
+ }
+
+ protected void addAudioSink(AudioSink audioSink) {
+ this.audioSinks.put(audioSink.toString(), audioSink);
+ }
+
+ protected void removeAudioSink(AudioSink audioSink) {
+ this.audioSinks.remove(audioSink.toString());
+ }
+
+ /**
+ * Retrieves a TTS service.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return a TTS service or null, if no service is available or if a default is configured, but no according service
+ * is found
+ */
+ public TTSService getTTS() {
+ TTSService tts = null;
+ if (defaultTTS != null) {
+ tts = ttsServices.get(defaultTTS);
+ if (tts == null) {
+ logger.warn("Default TTS service '{}' not available!", defaultTTS);
+ }
+ } else if (!ttsServices.isEmpty()) {
+ tts = ttsServices.values().iterator().next();
+ } else {
+ logger.debug("No TTS service available!");
+ }
+ return tts;
+ }
+
+ /**
+ * Retrieves a STT service.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return a STT service or null, if no service is available or if a default is configured, but no according service
+ * is found
+ */
+ public STTService getSTT() {
+ STTService stt = null;
+ if (defaultTTS != null) {
+ stt = sttServices.get(defaultSTT);
+ if (stt == null) {
+ logger.warn("Default STT service '{}' not available!", defaultSTT);
+ }
+ } else if (!sttServices.isEmpty()) {
+ stt = sttServices.values().iterator().next();
+ } else {
+ logger.debug("No STT service available!");
+ }
+ return stt;
+ }
+
+ /**
+ * Retrieves a KS service.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return a KS service or null, if no service is available or if a default is configured, but no according service
+ * is found
+ */
+ public KSService getKS() {
+ KSService ks = null;
+ if (defaultKS != null) {
+ ks = ksServices.get(defaultKS);
+ if (ks == null) {
+ logger.warn("Default KS service '{}' not available!", defaultKS);
+ }
+ } else if (!ksServices.isEmpty()) {
+ ks = ksServices.values().iterator().next();
+ } else {
+ logger.debug("No KS service available!");
+ }
+ return ks;
+ }
+
+ /**
+ * Retrieves a HumanLanguageInterpreter.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return a HumanLanguageInterpreter or null, if no service is available or if a default is configured, but no
+ * according service is found
+ */
+ public HumanLanguageInterpreter getHLI() {
+ HumanLanguageInterpreter hli = null;
+ if (defaultHLI != null) {
+ hli = humanLanguageInterpreters.get(defaultHLI);
+ if (hli == null) {
+ logger.warn("Default HumanLanguageInterpreter '{}' not available!", defaultHLI);
+ }
+ } else if (!humanLanguageInterpreters.isEmpty()) {
+ hli = humanLanguageInterpreters.values().iterator().next();
+ } else {
+ logger.debug("No HumanLanguageInterpreter available!");
+ }
+ return hli;
+ }
+
+ /**
+ * Retrieves an AudioSink.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return an AudioSink or null, if no service is available or if a default is configured, but no according service
+ * is found
+ */
+ public AudioSink getSink() {
+ AudioSink sink = null;
+ if (defaultSink != null) {
+ sink = audioSinks.get(defaultSink);
+ if (sink == null) {
+ logger.warn("Default AudioSink service '{}' not available!", defaultSink);
+ }
+ } else if (!audioSinks.isEmpty()) {
+ sink = audioSinks.values().iterator().next();
+ } else {
+ logger.debug("No AudioSink service available!");
+ }
+ return sink;
+ }
+
+ /**
+ * Retrieves an AudioSource.
+ * If a default name is configured and the service available, this is returned. Otherwise, the first available
+ * service is returned.
+ *
+ * @return an AudioSource or null, if no service is available or if a default is configured, but no according
+ * service is found
+ */
+ public AudioSource getSource() {
+ AudioSource source = null;
+ if (defaultSource != null) {
+ source = audioSources.get(defaultSource);
+ if (source == null) {
+ logger.warn("Default AudioSource service '{}' not available!", defaultSource);
+ }
+ } else if (!audioSources.isEmpty()) {
+ source = audioSources.values().iterator().next();
+ } else {
+ logger.debug("No AudioSource service available!");
+ }
+ return source;
+ }
+
+ /**
+ * Returns all available voices in the system from all TTS services.
+ *
+ * @return a set of available voices
+ */
+ public Set getAllVoices() {
+ Set voices = new HashSet<>();
+ for (TTSService tts : ttsServices.values()) {
+ voices.addAll(tts.getAvailableVoices());
+ }
+ return voices;
+ }
+
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java
new file mode 100644
index 00000000000..c96e509d3dd
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/DialogProcessor.java
@@ -0,0 +1,164 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.internal;
+
+import java.util.HashSet;
+import java.util.Locale;
+
+import org.eclipse.smarthome.core.audio.AudioException;
+import org.eclipse.smarthome.core.audio.AudioSink;
+import org.eclipse.smarthome.core.audio.AudioSource;
+import org.eclipse.smarthome.core.audio.AudioStream;
+import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException;
+import org.eclipse.smarthome.core.voice.KSErrorEvent;
+import org.eclipse.smarthome.core.voice.KSEvent;
+import org.eclipse.smarthome.core.voice.KSException;
+import org.eclipse.smarthome.core.voice.KSListener;
+import org.eclipse.smarthome.core.voice.KSService;
+import org.eclipse.smarthome.core.voice.KSpottedEvent;
+import org.eclipse.smarthome.core.voice.RecognitionStopEvent;
+import org.eclipse.smarthome.core.voice.STTEvent;
+import org.eclipse.smarthome.core.voice.STTException;
+import org.eclipse.smarthome.core.voice.STTListener;
+import org.eclipse.smarthome.core.voice.STTService;
+import org.eclipse.smarthome.core.voice.STTServiceHandle;
+import org.eclipse.smarthome.core.voice.SpeechRecognitionErrorEvent;
+import org.eclipse.smarthome.core.voice.SpeechRecognitionEvent;
+import org.eclipse.smarthome.core.voice.TTSException;
+import org.eclipse.smarthome.core.voice.TTSService;
+import org.eclipse.smarthome.core.voice.Voice;
+import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter;
+import org.eclipse.smarthome.core.voice.text.InterpretationException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An instance of this class can handle a complete dialog with the user. It orchestrates the keyword spotting, the stt
+ * and tts services together with the human language interpreter.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ */
+public class DialogProcessor implements KSListener, STTListener {
+
+ private final Logger logger = LoggerFactory.getLogger(DialogProcessor.class);
+
+ /**
+ * If the processor should spot new keywords
+ */
+ private boolean processing = true;
+
+ /**
+ * If the STT server is in the process of aborting
+ */
+ private boolean isSTTServerAborting = false;
+
+ private 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;
+
+ public DialogProcessor(KSService ks, STTService stt, TTSService tts, HumanLanguageInterpreter hli,
+ AudioSource source, AudioSink sink, Locale locale, String keyword) {
+ this.locale = locale;
+ this.ks = ks;
+ this.hli = hli;
+ this.stt = stt;
+ this.tts = tts;
+ this.source = source;
+ this.sink = sink;
+ this.keyword = keyword;
+ }
+
+ public void start() {
+ try {
+ ks.spot(this, source.getInputStream(), locale, this.keyword);
+ } catch (KSException | AudioException e) {
+ logger.error("Encountered error calling spot: {}", e.getMessage());
+ }
+ }
+
+ @Override
+ public void ksEventReceived(KSEvent ksEvent) {
+ if (!processing) {
+ processing = true;
+ this.isSTTServerAborting = false;
+ if (ksEvent instanceof KSpottedEvent) {
+ if (stt != null) {
+ try {
+ this.sttServiceHandle = stt.recognize(this, source.getInputStream(), this.locale,
+ new HashSet());
+ } catch (STTException | AudioException e) {
+ say("Error during recognition: " + e.getMessage());
+ }
+ }
+ } else if (ksEvent instanceof KSErrorEvent) {
+ KSErrorEvent kse = (KSErrorEvent) ksEvent;
+ say("Encountered error spotting keywords, " + kse.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public synchronized void sttEventReceived(STTEvent sttEvent) {
+ if (sttEvent instanceof SpeechRecognitionEvent) {
+ if (false == this.isSTTServerAborting) {
+ this.sttServiceHandle.abort();
+ this.isSTTServerAborting = true;
+ SpeechRecognitionEvent sre = (SpeechRecognitionEvent) sttEvent;
+ String question = sre.getTranscript();
+ try {
+ this.processing = false;
+ say(hli.interpret(this.locale, question));
+ } catch (InterpretationException e) {
+ say(e.getMessage());
+ }
+ }
+ } else if (sttEvent instanceof RecognitionStopEvent) {
+ this.processing = false;
+ } else if (sttEvent instanceof SpeechRecognitionErrorEvent) {
+ if (false == this.isSTTServerAborting) {
+ this.sttServiceHandle.abort();
+ this.isSTTServerAborting = true;
+ this.processing = false;
+ SpeechRecognitionErrorEvent sre = (SpeechRecognitionErrorEvent) sttEvent;
+ say("Encountered error: " + sre.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Says the passed command
+ *
+ * @param text The text to say
+ */
+ protected void say(String text) {
+ try {
+ Voice voice = null;
+ for (Voice currentVoice : tts.getAvailableVoices()) {
+ if (this.locale.getLanguage() == currentVoice.getLocale().getLanguage()) {
+ voice = currentVoice;
+ break;
+ }
+ }
+ if (null == voice) {
+ throw new TTSException("Unable to find a suitable voice");
+ }
+ AudioStream audioStream = tts.synthesize(text, voice, null);
+ sink.process(audioStream);
+ } catch (TTSException | UnsupportedAudioFormatException e) {
+ logger.error("Error saying '{}'", text);
+ }
+ }
+
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java
new file mode 100644
index 00000000000..8dda98b35fc
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/VoiceConsoleCommandExtension.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.internal;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.commons.lang.ArrayUtils;
+import org.eclipse.smarthome.core.items.Item;
+import org.eclipse.smarthome.core.items.ItemNotFoundException;
+import org.eclipse.smarthome.core.items.ItemNotUniqueException;
+import org.eclipse.smarthome.core.items.ItemRegistry;
+import org.eclipse.smarthome.core.voice.Voice;
+import org.eclipse.smarthome.core.voice.VoiceManager;
+import org.eclipse.smarthome.core.voice.text.HumanLanguageInterpreter;
+import org.eclipse.smarthome.core.voice.text.InterpretationException;
+import org.eclipse.smarthome.io.console.Console;
+import org.eclipse.smarthome.io.console.extensions.AbstractConsoleCommandExtension;
+
+/**
+ * Console command extension for all voice features.
+ *
+ * @author Kai Kreuzer - Initial contribution and API
+ *
+ */
+public class VoiceConsoleCommandExtension extends AbstractConsoleCommandExtension {
+
+ private static final String SUBCMD_SAY = "say";
+ private static final String SUBCMD_INTERPRET = "interpret";
+ private static final String SUBCMD_VOICES = "voices";
+
+ private ItemRegistry itemRegistry;
+ private VoiceManager voiceManager;
+
+ public VoiceConsoleCommandExtension() {
+ super("voice", "Commands around voice enablement features.");
+ }
+
+ @Override
+ public List getUsages() {
+ return Arrays.asList(new String[] { buildCommandUsage(SUBCMD_SAY + " ", "speaks a text"),
+ buildCommandUsage(SUBCMD_INTERPRET + " ", "interprets a human language command"),
+ buildCommandUsage(SUBCMD_VOICES, "lists available voices of the active TTS service") });
+
+ }
+
+ @Override
+ public void execute(String[] args, Console console) {
+ if (args.length > 0) {
+ String subCommand = args[0];
+ switch (subCommand) {
+ case SUBCMD_SAY:
+ if (args.length > 1) {
+ say((String[]) ArrayUtils.subarray(args, 1, args.length), console);
+ } else {
+ console.println("Specify text to say (e.g. 'say hello')");
+ }
+ return;
+ case SUBCMD_INTERPRET:
+ if (args.length > 1) {
+ interpret((String[]) ArrayUtils.subarray(args, 1, args.length), console);
+ } else {
+ console.println("Specify text to interpret (e.g. 'interpret turn all lights off')");
+ }
+ return;
+ case SUBCMD_VOICES:
+ for (Voice voice : voiceManager.getAllVoices()) {
+ console.println(voice.getUID() + " " + voice.getLabel() + " (" + voice.getLocale() + ")");
+ }
+ return;
+ default:
+ break;
+ }
+ } else {
+ printUsage(console);
+ }
+ }
+
+ private void interpret(String[] args, Console console) {
+ HumanLanguageInterpreter interpreter = voiceManager.getHLI();
+ if (interpreter != null) {
+ StringBuilder sb = new StringBuilder(args[0]);
+ for (int i = 1; i < args.length; i++) {
+ sb.append(" ");
+ sb.append(args[i]);
+ }
+ String msg = sb.toString();
+ try {
+ console.println(interpreter.interpret(Locale.getDefault(), msg));
+ } catch (InterpretationException ie) {
+ console.println(ie.getMessage());
+ }
+ } else {
+ console.println("No human language interpreter available!");
+ }
+ }
+
+ private void say(String[] args, Console console) {
+ StringBuilder msg = new StringBuilder();
+ for (String word : args) {
+ if (word.startsWith("%") && word.endsWith("%") && word.length() > 2) {
+ String itemName = word.substring(1, word.length() - 1);
+ try {
+ Item item = this.itemRegistry.getItemByPattern(itemName);
+ msg.append(item.getState().toString());
+ } catch (ItemNotFoundException e) {
+ console.println("Error: Item '" + itemName + "' does not exist.");
+ } catch (ItemNotUniqueException e) {
+ console.print("Error: Multiple items match this pattern: ");
+ for (Item item : e.getMatchingItems()) {
+ console.print(item.getName() + " ");
+ }
+ }
+ } else {
+ msg.append(word);
+ }
+ msg.append(" ");
+ }
+ voiceManager.say(msg.toString());
+ }
+
+ protected void setItemRegistry(ItemRegistry itemRegistry) {
+ this.itemRegistry = itemRegistry;
+ }
+
+ protected void unsetItemRegistry(ItemRegistry itemRegistry) {
+ this.itemRegistry = null;
+ }
+
+ protected void setVoiceManager(VoiceManager voiceManager) {
+ this.voiceManager = voiceManager;
+ }
+
+ protected void unsetVoiceManager(VoiceManager voiceManager) {
+ this.voiceManager = null;
+ }
+
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java
new file mode 100644
index 00000000000..9ba6f752ee2
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/internal/text/StandardInterpreter.java
@@ -0,0 +1,132 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.internal.text;
+
+import java.util.Locale;
+
+import org.eclipse.smarthome.core.library.types.HSBType;
+import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType;
+import org.eclipse.smarthome.core.library.types.NextPreviousType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.OpenClosedType;
+import org.eclipse.smarthome.core.library.types.PlayPauseType;
+import org.eclipse.smarthome.core.library.types.RewindFastforwardType;
+import org.eclipse.smarthome.core.library.types.StopMoveType;
+import org.eclipse.smarthome.core.library.types.UpDownType;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.eclipse.smarthome.core.voice.text.AbstractRuleBasedInterpreter;
+import org.eclipse.smarthome.core.voice.text.Expression;
+
+/**
+ * A human language command interpretation service.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+public class StandardInterpreter extends AbstractRuleBasedInterpreter {
+
+ @Override
+ public void createRules() {
+
+ /****************************** ENGLISH ******************************/
+
+ Expression onOff = alt(cmd("on", OnOffType.ON), cmd("off", OnOffType.OFF));
+ Expression turn = alt("turn", "switch");
+ Expression put = alt("put", "bring");
+ Expression of = opt("of");
+ Expression the = opt("the");
+ Expression to = opt("to");
+ Expression color = alt(cmd("white", HSBType.WHITE), cmd("pink", HSBType.fromRGB(255, 96, 208)),
+ cmd("yellow", HSBType.fromRGB(255, 224, 32)), cmd("orange", HSBType.fromRGB(255, 160, 16)),
+ cmd("purple", HSBType.fromRGB(128, 0, 128)), cmd("red", HSBType.RED), cmd("green", HSBType.GREEN),
+ cmd("blue", HSBType.BLUE));
+
+ addRules(Locale.ENGLISH,
+
+ /* OnOffType */
+
+ itemRule(seq(turn, the), /* item */ onOff),
+
+ itemRule(seq(turn, onOff) /* item */),
+
+ /* OpenCloseType */
+
+ itemRule(seq(cmd("open", OpenClosedType.OPEN), the) /* item */),
+
+ itemRule(seq(cmd("close", OpenClosedType.CLOSED), the) /* item */),
+
+ /* IncreaseDecreaseType */
+
+ itemRule(seq(cmd(alt("dim", "decrease", "lower", "soften"), IncreaseDecreaseType.DECREASE),
+ the) /* item */),
+
+ itemRule(seq(cmd(alt("brighten", "increase", "harden", "enhance"), IncreaseDecreaseType.INCREASE),
+ the) /* item */),
+
+ /* ColorType */
+
+ itemRule(seq(opt("set"), the, opt("color"), of, the), /* item */ seq(to, color)),
+
+ /* UpDownType */
+
+ itemRule(seq(put, the), /* item */ cmd("up", UpDownType.UP)),
+
+ itemRule(seq(put, the), /* item */ cmd("down", UpDownType.DOWN)),
+
+ /* NextPreviousType */
+
+ itemRule("move",
+ /* item */ seq(opt("to"),
+ alt(cmd("next", NextPreviousType.NEXT), cmd("previous", NextPreviousType.PREVIOUS)))),
+
+ /* PlayPauseType */
+
+ itemRule(seq(cmd("play", PlayPauseType.PLAY), the) /* item */),
+
+ itemRule(seq(cmd("pause", PlayPauseType.PAUSE), the) /* item */),
+
+ /* RewindFastForwardType */
+
+ itemRule(seq(cmd("rewind", RewindFastforwardType.REWIND), the) /* item */),
+
+ itemRule(seq(cmd(seq(opt("fast"), "forward"), RewindFastforwardType.FASTFORWARD), the) /* item */),
+
+ /* StopMoveType */
+
+ itemRule(seq(cmd("stop", StopMoveType.STOP), the) /* item */),
+
+ itemRule(seq(cmd(alt("start", "move", "continue"), StopMoveType.MOVE), the) /* item */),
+
+ /* RefreshType */
+
+ itemRule(seq(cmd("refresh", RefreshType.REFRESH), the) /* item */)
+
+ );
+
+ /****************************** GERMAN ******************************/
+
+ Expression einAnAus = alt(cmd("ein", OnOffType.ON), cmd("an", OnOffType.ON), cmd("aus", OnOffType.OFF));
+ Expression denDieDas = opt(alt("den", "die", "das"));
+
+ addRules(Locale.GERMAN, itemRule(seq(alt("schalt", "schalte", "mach"), denDieDas), /* item */ einAnAus),
+ itemRule(seq(cmd("öffne", OpenClosedType.OPEN), denDieDas) /* item */),
+ itemRule(seq(cmd(alt("schließ", "schließe"), OpenClosedType.OPEN), denDieDas) /* item */));
+
+ }
+
+ @Override
+ public String getId() {
+ return "system";
+ }
+
+ @Override
+ public String getLabel(Locale locale) {
+ return "Built-in Interpreter";
+ }
+
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java
new file mode 100644
index 00000000000..a7ad3fd895f
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ASTNode.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.text;
+
+/**
+ * Abstract syntax tree node. Result of parsing an expression.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+public class ASTNode {
+
+ private boolean success = false;
+ private ASTNode[] children;
+ private TokenList remainingTokens;
+
+ private String name;
+ private Object value;
+ private Object tag;
+
+ public ASTNode() {
+ }
+
+ /**
+ * Constructs a new AST node.
+ *
+ * @param children the node's children
+ * @param remainingTokens remaining token list starting with the first token that was not covered/consumed
+ */
+ public ASTNode(ASTNode[] children, TokenList remainingTokens) {
+ this.success = true;
+ this.children = children;
+ this.remainingTokens = remainingTokens;
+ }
+
+ /**
+ * Breadth searching this (sub-) tree/node for a node with the given name.
+ *
+ * @param name the name that's used for looking up the tree
+ * @return first node with the given name or null, if none was found
+ */
+ public ASTNode findNode(String name) {
+ if (this.name != null && this.name.equals(name)) {
+ return this;
+ }
+ ASTNode n;
+ for (ASTNode sn : children) {
+ n = sn.findNode(name);
+ if (n != null) {
+ return n;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @return the value of this node as {@link String[]}
+ */
+ public String[] getValueAsStringArray() {
+ Object[] objs = value instanceof Object[] ? (Object[]) value : new Object[] {
+ value
+ };
+ String[] result = new String[objs.length];
+ for (int i = 0; i < objs.length; i++) {
+ result[i] = objs[i] == null ? "" : ("" + objs[i]);
+ }
+ return result;
+ }
+
+ /**
+ * @return the value of this node as {@link String}.
+ */
+ public String getValueAsString() {
+ return value == null ? "" : ("" + value);
+ }
+
+ /**
+ * Breadth searches this (sub-) tree/node for a node with the given name and returning its value as a
+ * {@link String[]}.
+ *
+ * @param name the name of the named node to be found
+ * @return the value of the resulting node as {@link String[]} or null if not found
+ */
+ public String[] findValueAsStringArray(String name) {
+ ASTNode node = findNode(name);
+ return node == null ? null : node.getValueAsStringArray();
+ }
+
+ /**
+ * Breadth searches this (sub-) tree/node for a node with the given name and returning its value as a {@link String}
+ * .
+ *
+ * @param name the name of the named node to be found
+ * @return the value of the resulting node as {@link String} or null if not found
+ */
+ public String findValueAsString(String name) {
+ ASTNode node = findNode(name);
+ return node == null ? null : node.getValueAsString();
+ }
+
+ /**
+ * Breadth searches this (sub-) tree/node for a node with the given name and type and returning its value.
+ *
+ * @param name the name of the named node to be found
+ * @param cls the node's value has to be assignable to a reference of this class to match during search
+ * @return the value of the resulting node. Null, if not found or the value does not match {@link cls}.
+ */
+ public Object findValue(String name, Class> cls) {
+ ASTNode node = findNode(name);
+ return node == null ? null
+ : ((node.value != null && cls.isAssignableFrom(node.value.getClass())) ? node.value : null);
+ }
+
+ /**
+ * Breadth searches this (sub-) tree/node for a node with the given name and returning its value.
+ *
+ * @param name the name of the named node to be found
+ * @return the value of the resulting node. Null, if not found.
+ */
+ public Object findValue(String name) {
+ ASTNode node = findNode(name);
+ return node == null ? null : node.value;
+ }
+
+ /**
+ * @return if the node is a valid one (true) or parsing was not successful (false)
+ */
+ public boolean isSuccess() {
+ return success;
+ }
+
+ /**
+ * @param success if the node is a valid one (true) or parsing was not successful (false)
+ */
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ /**
+ * @return the children
+ */
+ public ASTNode[] getChildren() {
+ return children;
+ }
+
+ /**
+ * @param children the children to set
+ */
+ public void setChildren(ASTNode[] children) {
+ this.children = children;
+ }
+
+ /**
+ * @return the remainingTokens
+ */
+ public TokenList getRemainingTokens() {
+ return remainingTokens;
+ }
+
+ /**
+ * @param remainingTokens the remainingTokens to set
+ */
+ public void setRemainingTokens(TokenList remainingTokens) {
+ this.remainingTokens = remainingTokens;
+ }
+
+ /**
+ * @return the name
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @param name the name to set
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * @return the value
+ */
+ public Object getValue() {
+ return value;
+ }
+
+ /**
+ * @param value the value to set
+ */
+ public void setValue(Object value) {
+ this.value = value;
+ }
+
+ /**
+ * @return the tag
+ */
+ public Object getTag() {
+ return tag;
+ }
+
+ /**
+ * @param tag the tag to set
+ */
+ public void setTag(Object tag) {
+ this.tag = tag;
+ }
+}
\ No newline at end of file
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java
new file mode 100644
index 00000000000..042d395dff4
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/AbstractRuleBasedInterpreter.java
@@ -0,0 +1,842 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.text;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import org.eclipse.smarthome.core.common.registry.RegistryChangeListener;
+import org.eclipse.smarthome.core.events.EventPublisher;
+import org.eclipse.smarthome.core.items.GroupItem;
+import org.eclipse.smarthome.core.items.Item;
+import org.eclipse.smarthome.core.items.ItemRegistry;
+import org.eclipse.smarthome.core.items.events.ItemEventFactory;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.State;
+
+/**
+ * A human language command interpretation service.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+public abstract class AbstractRuleBasedInterpreter implements HumanLanguageInterpreter {
+
+ private static final String JSGF = "JSGF";
+ private static final Set supportedGrammars = Collections.unmodifiableSet(Collections.singleton(JSGF));
+
+ private static final String OK = "ok";
+ private static final String SORRY = "sorry";
+
+ private static final String CMD = "cmd";
+ private static final String NAME = "name";
+
+ private static final String LANGUAGE_SUPPORT = "LanguageSupport";
+
+ private HashMap> languageRules;
+ private HashMap> allItemTokens = null;
+ private HashMap>>> itemTokens = null;
+
+ private ItemRegistry itemRegistry;
+ private EventPublisher eventPublisher;
+
+ private RegistryChangeListener registryChangeListener = new RegistryChangeListener() {
+ @Override
+ public void added(Item element) {
+ invalidate();
+ }
+
+ @Override
+ public void removed(Item element) {
+ invalidate();
+ }
+
+ @Override
+ public void updated(Item oldElement, Item element) {
+ invalidate();
+ }
+ };
+
+ /**
+ * Called whenever the rules are to be (re)generated and added by {@link addRules}
+ */
+ protected abstract void createRules();
+
+ @Override
+ public String interpret(Locale locale, String text) throws InterpretationException {
+ ResourceBundle language = ResourceBundle.getBundle(LANGUAGE_SUPPORT, locale);
+ Rule[] rules = getRules(locale);
+ if (language == null || rules.length == 0) {
+ throw new InterpretationException(
+ locale.getDisplayLanguage(Locale.ENGLISH) + " is not supported at the moment.");
+ }
+ TokenList tokens = new TokenList(tokenize(locale, text));
+ if (tokens.eof()) {
+ throw new InterpretationException(language.getString(SORRY));
+ }
+ InterpretationResult result;
+
+ InterpretationResult lastResult = null;
+ for (Rule rule : rules) {
+ if ((result = rule.execute(language, tokens)).isSuccess()) {
+ return result.getResponse();
+ } else {
+ if (result != InterpretationResult.SYNTAX_ERROR) {
+ lastResult = result;
+ }
+ }
+ }
+ if (lastResult == null) {
+ throw new InterpretationException(language.getString(SORRY));
+ } else {
+ throw lastResult.getException();
+ }
+ }
+
+ private void invalidate() {
+ allItemTokens = null;
+ itemTokens = null;
+ languageRules = null;
+ }
+
+ /**
+ * All the tokens (name parts) of the names of all the items in the {@link ItemRegistry}.
+ *
+ * @param locale The locale that is to be used for preparing the tokens.
+ * @return the identifier tokens
+ */
+ HashSet getAllItemTokens(Locale locale) {
+ if (allItemTokens == null) {
+ allItemTokens = new HashMap>();
+ }
+ HashSet localeTokens = allItemTokens.get(locale);
+ if (localeTokens == null) {
+ allItemTokens.put(locale, localeTokens = new HashSet());
+ for (Item item : itemRegistry.getAll()) {
+ localeTokens.addAll(tokenize(locale, item.getLabel()));
+ }
+ }
+ return localeTokens;
+ }
+
+ /**
+ * Retrieves the list of identifier token sets per item currently contained in the {@link ItemRegistry}.
+ * Each item entry in the resulting hash map will feature a list of different token sets. Each token set
+ * represents one possible way "through" a chain of parent groups, where each groups tokenized name is
+ * part of the set.
+ *
+ * @param locale The locale that is to be used for preparing the tokens.
+ * @return the list of identifier token sets per item
+ */
+ HashMap>> getItemTokens(Locale locale) {
+ if (itemTokens == null) {
+ itemTokens = new HashMap>>>();
+ }
+ HashMap>> localeTokens = itemTokens.get(locale);
+ if (localeTokens == null) {
+ itemTokens.put(locale, localeTokens = new HashMap>>());
+ for (Item item : itemRegistry.getItems()) {
+ if (item.getGroupNames().isEmpty()) {
+ addItem(locale, localeTokens, new HashSet(), item);
+ }
+ }
+ }
+ return localeTokens;
+ }
+
+ private void addItem(Locale locale, HashMap>> target, HashSet tokens,
+ Item item) {
+ HashSet nt = new HashSet(tokens);
+ nt.addAll(tokenize(locale, item.getLabel()));
+ ArrayList> list = target.get(item);
+ if (list == null) {
+ target.put(item, list = new ArrayList>());
+ }
+ list.add(nt);
+ if (item instanceof GroupItem) {
+ for (Item member : ((GroupItem) item).getMembers()) {
+ addItem(locale, target, nt, member);
+ }
+ }
+ }
+
+ /**
+ * Creates an item name placeholder expression. This expression is greedy: Only use it, if there are no other
+ * expressions following this one.
+ * It's safer to use {@link thingRule} instead.
+ *
+ * @return Expression that represents a name of an item.
+ */
+ protected Expression name() {
+ return name(null);
+ }
+
+ /**
+ * Creates an item name placeholder expression. This expression is greedy: Only use it, if you are able to pass in
+ * all possible stop tokens as excludes.
+ * It's safer to use {@link thingRule} instead.
+ *
+ * @param stopper Stop expression that, if matching, will stop this expression from consuming further tokens.
+ * @return Expression that represents a name of an item.
+ */
+ protected Expression name(Expression stopper) {
+ return tag(NAME, star(new ExpressionIdentifier(this, stopper)));
+ }
+
+ private HashMap> getLanguageRules() {
+ if (languageRules == null) {
+ languageRules = new HashMap>();
+ createRules();
+ }
+ return languageRules;
+ }
+
+ /**
+ * Retrieves all {@link Rule}s to a given {@link Locale}. It also retrieves all the same-language rules into greater
+ * indexes of the array (lower match priority).
+ *
+ * @param locale Locale filter
+ * @return Rules in descending match priority order.
+ */
+ public Rule[] getRules(Locale locale) {
+ HashMap> lr = getLanguageRules();
+ ArrayList rules = new ArrayList();
+ HashSet> ruleSets = new HashSet>();
+ ArrayList ruleSet = lr.get(locale);
+ if (ruleSet != null) {
+ ruleSets.add(ruleSet);
+ rules.addAll(ruleSet);
+ }
+
+ String l = locale.getLanguage();
+ for (Locale rl : lr.keySet()) {
+ if (rl.getLanguage().equals(l)) {
+ ruleSet = lr.get(rl);
+ if (!ruleSets.contains(ruleSet)) {
+ ruleSets.add(ruleSet);
+ rules.addAll(ruleSet);
+ }
+ }
+ }
+ return rules.toArray(new Rule[0]);
+ }
+
+ /**
+ * Adds {@link Locale} specific rules to this interpreter. To be called from within {@link createRules}.
+ *
+ * @param locale Locale of the rules.
+ * @param rules Rules to add.
+ */
+ protected void addRules(Locale locale, Rule... rules) {
+ ArrayList ruleSet = languageRules.get(locale);
+ if (ruleSet == null) {
+ languageRules.put(locale, ruleSet = new ArrayList());
+ }
+ for (Rule rule : rules) {
+ ruleSet.add(rule);
+ }
+ }
+
+ /**
+ * Creates an item rule on base of an expression, where the tail of the new rule's expression will consist of an
+ * item
+ * name expression.
+ *
+ * @param headExpression The head expression that should contain at least one {@link cmd} generated expression. The
+ * corresponding {@link Command} will in case of a match be sent to the matching {@link Item}.
+ * @return The created rule.
+ */
+ protected Rule itemRule(Object headExpression) {
+ return itemRule(headExpression, null);
+ }
+
+ /**
+ * Creates an item rule on base of a head and a tail expression, where the middle part of the new rule's expression
+ * will consist of an item
+ * name expression. Either the head expression or the tail expression should contain at least one {@link cmd}
+ * generated expression.
+ *
+ * @param headExpression The head expression.
+ * @param tailExpression The tail expression.
+ * @return The created rule.
+ */
+ protected Rule itemRule(Object headExpression, Object tailExpression) {
+ Expression tail = exp(tailExpression);
+ Expression expression = tail == null ? seq(headExpression, name()) : seq(headExpression, name(tail), tail);
+ return new Rule(expression) {
+ @Override
+ public InterpretationResult interpretAST(ResourceBundle language, ASTNode node) {
+ String[] name = node.findValueAsStringArray(NAME);
+ ASTNode cmdNode = node.findNode(CMD);
+ Object tag = cmdNode.getTag();
+ Object value = cmdNode.getValue();
+ Command command;
+ if (tag instanceof Command) {
+ command = (Command) tag;
+ } else if (value instanceof Number) {
+ command = new DecimalType(((Number) value).longValue());
+ } else {
+ command = new StringType(cmdNode.getValueAsString());
+ }
+ if (name != null && command != null) {
+ try {
+ return new InterpretationResult(true, executeSingle(language, name, command));
+ } catch (InterpretationException ex) {
+ return new InterpretationResult(ex);
+ }
+ }
+ return InterpretationResult.SEMANTIC_ERROR;
+ }
+ };
+
+ }
+
+ /**
+ * Converts an object to an expression. Objects that are already instances of {@link Expression} are just returned.
+ * All others are converted to {@link match} expressions.
+ *
+ * @param obj the object that's to be converted
+ * @return resulting expression
+ */
+ protected Expression exp(Object obj) {
+ if (obj instanceof Expression) {
+ return (Expression) obj;
+ } else {
+ return obj == null ? null : new ExpressionMatch(obj.toString());
+ }
+ }
+
+ /**
+ * Converts all parameters to an expression array. Objects that are already instances of {@link Expression} are not
+ * touched.
+ * All others are converted to {@link match} expressions.
+ *
+ * @param obj the objects that are to be converted
+ * @return resulting expression array
+ */
+ protected Expression[] exps(Object... objects) {
+ ArrayList result = new ArrayList();
+ for (int i = 0; i < objects.length; i++) {
+ Expression e = exp(objects[i]);
+ if (e != null) {
+ result.add(e);
+ }
+ }
+ return result.toArray(new Expression[0]);
+ }
+
+ /**
+ * Adds a name to the resulting AST tree, if the given expression matches.
+ *
+ * @param name name to add
+ * @param expression the expression that has to match
+ * @return resulting expression
+ */
+ protected Expression tag(String name, Object expression) {
+ return tag(name, expression, null);
+ }
+
+ /**
+ * Adds a value to the resulting AST tree, if the given expression matches.
+ *
+ * @param expression the expression that has to match
+ * @param tag the tag that's to be set
+ * @return resulting expression
+ */
+ protected Expression tag(Object expression, Object tag) {
+ return tag(null, expression, tag);
+ }
+
+ /**
+ * Adds a name and a tag to the resulting AST tree, if the given expression matches.
+ *
+ * @param name name to add
+ * @param expression the expression that has to match
+ * @param tag the tag that's to be set
+ * @return resulting expression
+ */
+ protected Expression tag(String name, Object expression, Object tag) {
+ return new ExpressionLet(name, exp(expression), null, tag);
+ }
+
+ /**
+ * Adds a command to the resulting AST tree. If the expression evaluates to a
+ * numeric value, it will get a {@link DecimalType}, otherwise a {@link StringType}.
+ *
+ * @param expression the expression that has to match
+ * @return resulting expression
+ */
+ protected Expression cmd(Object expression) {
+ return cmd(expression, null);
+ }
+
+ /**
+ * Adds a command to the resulting AST tree, if the expression matches.
+ *
+ * @param expression the expression that has to match
+ * @param command the command that should be added
+ * @return resulting expression
+ */
+ protected Expression cmd(Object expression, Command command) {
+ return tag(CMD, expression, command);
+ }
+
+ /**
+ * Creates an alternatives expression. Matches, as soon as one of the given expressions matches. They are tested in
+ * the provided order. The value of the matching expression will be used for the resulting nodes's value.
+ *
+ * @param expressions the expressions (alternatives) that are to be tested
+ * @return resulting expression
+ */
+ protected ExpressionAlternatives alt(Object... expressions) {
+ return new ExpressionAlternatives(exps(expressions));
+ }
+
+ /**
+ * Creates a sequence expression. Matches, if all the given expressions match. They are tested in
+ * the provided order. The resulting nodes's value will be an {@link Object[]} that contains all values of the
+ * matching expressions.
+ *
+ * @param expressions the expressions (alternatives) that have to match in sequence
+ * @return resulting expression
+ */
+ protected ExpressionSequence seq(Object... expressions) {
+ return new ExpressionSequence(exps(expressions));
+ }
+
+ /**
+ * Creates an optional expression. Always succeeds. The resulting nodes's value will be the one of the
+ * matching expression or null.
+ *
+ * @param expression the optionally matching expression
+ * @return resulting expression
+ */
+ protected ExpressionCardinality opt(Object expression) {
+ return new ExpressionCardinality(exp(expression), false, true);
+ }
+
+ /**
+ * Creates a repeating expression that will match the given expression as often as possible. Always succeeds. The
+ * resulting node's value will be an {@link Object[]} that contains all values of the
+ * matches.
+ *
+ * @param expression the repeating expression
+ * @return resulting expression
+ */
+ protected ExpressionCardinality star(Object expression) {
+ return new ExpressionCardinality(exp(expression), false, false);
+ }
+
+ /**
+ * Creates a repeating expression that will match the given expression as often as possible. Only succeeds, if there
+ * is at least one match. The resulting node's value will be an {@link Object[]} that contains all values of the
+ * matches.
+ *
+ * @param expression the repeating expression
+ * @return resulting expression
+ */
+ protected ExpressionCardinality plus(Object expression) {
+ return new ExpressionCardinality(exp(expression), true, false);
+ }
+
+ /**
+ * Executes a command on one item that's to be found in the item registry by given name fragments.
+ * Fails, if there is more than on item.
+ *
+ * @param language resource bundle used for producing localized response texts
+ * @param labelFragments label fragments that are used to match an item's label.
+ * For a positive match, the item's label has to contain every fragment - independently of their order.
+ * They are treated case insensitive.
+ * @param command command that should be executed
+ * @return response text
+ * @throws InterpretationException in case that there is no or more than on item matching the fragments
+ */
+ protected String executeSingle(ResourceBundle language, String[] labelFragments, Command command)
+ throws InterpretationException {
+ ArrayList items = getMatchingItems(language, labelFragments, command.getClass());
+ if (items.size() < 1) {
+ throw new InterpretationException(language.getString("no_objects"));
+ } else if (items.size() > 1) {
+ throw new InterpretationException(language.getString("multiple_objects"));
+ } else {
+ Item item = items.get(0);
+ if (command instanceof State) {
+ try {
+ State newState = (State) command;
+ State oldState = item.getStateAs(newState.getClass());
+ if (oldState.equals(newState)) {
+ String template = language.getString("state_already_singular");
+ String cmdName = "state_" + command.toString().toLowerCase();
+ String stateText = language.getString(cmdName);
+ return template.replace("", stateText);
+ }
+ } catch (Exception ex) {
+ }
+ }
+ eventPublisher.post(ItemEventFactory.createCommandEvent(item.getName(), command));
+ return language.getString(OK);
+ }
+ }
+
+ /**
+ * Filters the item registry by matching each item's name with the provided name fragments.
+ * The item's label and its parent group's labels are tokenizend {@link tokenize} and then altogether looked up
+ * by each and every provided fragment.
+ * For the item to get included into the result list, every provided fragment has to be found among the label
+ * tokens.
+ * If a command type is provided, the item also has to support it.
+ * In case of channels and their owners being ambiguous due to sharing most of the label sequence, only the top
+ * most item with support for the
+ * given command type is kept.
+ *
+ * @param language Language information that is used for matching
+ * @param labelFragments label fragments that are used to match an item's label.
+ * For a positive match, the item's label has to contain every fragment - independently of their order.
+ * They are treated case insensitive.
+ * @param commandType optional command type that all items have to support.
+ * Provide {null} if there is no need for a certain command to be supported.
+ * @return All matching items from the item registry.
+ */
+ protected ArrayList getMatchingItems(ResourceBundle language, String[] labelFragments, Class> commandType) {
+ ArrayList items = new ArrayList();
+ HashMap>> map = getItemTokens(language.getLocale());
+ for (Item item : map.keySet()) {
+ for (HashSet parts : map.get(item)) {
+ boolean allMatch = true;
+ for (String fragment : labelFragments) {
+ if (!parts.contains(fragment.toLowerCase(language.getLocale()))) {
+ allMatch = false;
+ break;
+ }
+ }
+ if (allMatch) {
+ if (commandType == null || item.getAcceptedCommandTypes().contains(commandType)) {
+ String name = item.getName();
+ boolean insert = true;
+ for (Item si : items) {
+ if (name.startsWith(si.getName())) {
+ insert = false;
+ }
+ }
+ if (insert) {
+ for (int i = 0; i < items.size(); i++) {
+ Item si = items.get(i);
+ if (si.getName().startsWith(name)) {
+ items.remove(i);
+ i--;
+ }
+ }
+ items.add(item);
+ }
+ }
+ }
+ }
+ }
+ return items;
+ }
+
+ /**
+ * Tokenizes text. Filters out all unsupported punctuation. Tokens will be lower case.
+ *
+ * @param locale the locale that should be used for lower casing
+ * @param text the text that should be tokenized
+ * @return resulting tokens
+ */
+ protected ArrayList tokenize(Locale locale, String text) {
+ ArrayList parts = new ArrayList();
+ if (text == null) {
+ return parts;
+ }
+ String[] split = text.toLowerCase(locale).replaceAll("[\\']", "").replaceAll("[^\\w\\s]", " ").split("\\s");
+ for (int i = 0; i < split.length; i++) {
+ String part = split[i].trim();
+ if (part.length() > 0) {
+ parts.add(part);
+ }
+ }
+ return parts;
+ }
+
+ @Override
+ public Set getSupportedLocales() {
+ return Collections.unmodifiableSet(getLanguageRules().keySet());
+ }
+
+ @Override
+ public Set getSupportedGrammarFormats() {
+ return supportedGrammars;
+ }
+
+ /**
+ * Helper class to generate a JSGF grammar from the rules of the interpreter.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+ private class JSGFGenerator {
+
+ private ResourceBundle language;
+
+ private HashMap ids = new HashMap();
+ private HashSet exported = new HashSet();
+ private HashSet shared = new HashSet();
+ private int counter = 0;
+
+ private HashSet identifierExcludes = new HashSet();
+ private HashSet identifiers = new HashSet();
+
+ private StringBuilder builder = new StringBuilder();
+
+ JSGFGenerator(ResourceBundle language) {
+ this.language = language;
+ }
+
+ private void addChildren(Expression exp) {
+ for (Expression se : exp.getChildExpressions()) {
+ addExpression(se);
+ }
+ }
+
+ private int addExpression(Expression exp) {
+ if (ids.containsKey(exp)) {
+ shared.add(exp);
+ return ids.get(exp);
+ } else {
+ int id = counter++;
+ ids.put(exp, id);
+ addChildren(exp);
+ return id;
+ }
+ }
+
+ private int addExportedExpression(Expression exp) {
+ shared.add(exp);
+ exported.add(exp);
+ int id = addExpression(exp);
+ return id;
+ }
+
+ private Expression unwrapLet(Expression expression) {
+ while (expression instanceof ExpressionLet) {
+ expression = ((ExpressionLet) expression).getSubExpression();
+ }
+ return expression;
+ }
+
+ private void emit(Object obj) {
+ builder.append(obj);
+ }
+
+ private void emitName(Expression expression) {
+ emit("r");
+ emit(ids.get(unwrapLet(expression)));
+ }
+
+ private void emitReference(Expression expression) {
+ emit("<");
+ emitName(expression);
+ emit(">");
+ }
+
+ private void emitDefinition(Expression expression) {
+ if (exported.contains(expression)) {
+ emit("public ");
+ }
+ emit("<");
+ emitName(expression);
+ emit("> = ");
+ emitExpression(expression);
+ emit(";\n\n");
+ }
+
+ private void emitUse(Expression expression) {
+ if (shared.contains(expression)) {
+ emitReference(expression);
+ } else {
+ emitExpression(expression);
+ }
+ }
+
+ private void emitExpression(Expression expression) {
+ expression = unwrapLet(expression);
+ if (expression instanceof ExpressionMatch) {
+ emitMatchExpression((ExpressionMatch) expression);
+ } else if (expression instanceof ExpressionSequence) {
+ emitSequenceExpression((ExpressionSequence) expression);
+ } else if (expression instanceof ExpressionAlternatives) {
+ emitAlternativesExpression((ExpressionAlternatives) expression);
+ } else if (expression instanceof ExpressionCardinality) {
+ emitCardinalExpression((ExpressionCardinality) expression);
+ } else if (expression instanceof ExpressionIdentifier) {
+ emitItemIdentifierExpression((ExpressionIdentifier) expression);
+ }
+ }
+
+ private void emitMatchExpression(ExpressionMatch expression) {
+ emit(expression.getPattern());
+ }
+
+ private void emitSequenceExpression(ExpressionSequence expression) {
+ emitGroup(" ", expression.getChildExpressions());
+ }
+
+ private void emitAlternativesExpression(ExpressionAlternatives expression) {
+ emitGroup(" | ", expression.getChildExpressions());
+ }
+
+ private void emitCardinalExpression(ExpressionCardinality expression) {
+ if (!expression.isAtLeastOne() && !expression.isAtMostOne()) {
+ emitUse(expression.getSubExpression());
+ emit("*");
+ } else if (expression.isAtLeastOne()) {
+ emitUse(expression.getSubExpression());
+ emit("+");
+ } else if (expression.isAtMostOne()) {
+ emit("[");
+ emitUse(expression.getSubExpression());
+ emit("]");
+ } else {
+ emitUse(expression.getSubExpression());
+ }
+ }
+
+ private void emitItemIdentifierExpression(ExpressionIdentifier expression) {
+ HashSet remainder = new HashSet(identifierExcludes);
+ Expression stopper = expression.getStopper();
+ HashSet excludes = stopper == null ? new HashSet() : stopper.getFirsts(language);
+ if (excludes.size() > 0) {
+ remainder.removeAll(excludes);
+ if (remainder.size() > 0) {
+ emit("(");
+ }
+ emit("");
+ for (String token : remainder) {
+ emit(" | ");
+ emit(token);
+ }
+ if (remainder.size() > 0) {
+ emit(")");
+ }
+ } else {
+ emit("");
+ }
+ }
+
+ private void emitGroup(String separator, List expressions) {
+ int l = expressions.size();
+ if (l > 0) {
+ emit("(");
+ }
+ for (int i = 0; i < l; i++) {
+ if (i > 0) {
+ emit(separator);
+ }
+ emitUse(expressions.get(i));
+ }
+ if (l > 0) {
+ emit(")");
+ }
+ }
+
+ private void emitSet(HashSet set, String separator) {
+ boolean sep = false;
+ for (String p : set) {
+ if (sep) {
+ emit(separator);
+ } else {
+ sep = true;
+ }
+ emit(p);
+ }
+ }
+
+ String getGrammar() {
+ Rule[] rules = getRules(language.getLocale());
+ identifiers.addAll(getAllItemTokens(language.getLocale()));
+ for (Rule rule : rules) {
+ Expression e = rule.getExpression();
+ addExportedExpression(e);
+ }
+ for (Expression e : ids.keySet()) {
+ if (e instanceof ExpressionIdentifier) {
+ Expression stopper = ((ExpressionIdentifier) e).getStopper();
+ if (stopper != null) {
+ identifierExcludes.addAll(stopper.getFirsts(language));
+ }
+ }
+ }
+
+ emit("#JSGF V1.0;\n\n");
+
+ if (identifierExcludes.size() > 0) {
+ HashSet identifierBase = new HashSet(identifiers);
+ identifierBase.removeAll(identifierExcludes);
+ emit(" = ");
+ emitSet(identifierBase, " | ");
+ emit(";\n\n = | ");
+ emitSet(identifierExcludes, " | ");
+ emit(";\n\n");
+ } else {
+ emit(" = ");
+ emitSet(identifiers, " | ");
+ emit(";\n\n");
+ }
+
+ for (Expression e : shared) {
+ emitDefinition(e);
+ }
+ String grammar = builder.toString();
+ return grammar;
+ }
+
+ }
+
+ @Override
+ public String getGrammar(Locale locale, String format) {
+ if (format != JSGF) {
+ return null;
+ }
+ JSGFGenerator generator = new JSGFGenerator(ResourceBundle.getBundle(LANGUAGE_SUPPORT, locale));
+ return generator.getGrammar();
+ }
+
+ public void setItemRegistry(ItemRegistry itemRegistry) {
+ if (this.itemRegistry == null) {
+ this.itemRegistry = itemRegistry;
+ this.itemRegistry.addRegistryChangeListener(registryChangeListener);
+ }
+ }
+
+ public void unsetItemRegistry(ItemRegistry itemRegistry) {
+ if (itemRegistry == this.itemRegistry) {
+ this.itemRegistry.removeRegistryChangeListener(registryChangeListener);
+ this.itemRegistry = null;
+ }
+ }
+
+ public void setEventPublisher(EventPublisher eventPublisher) {
+ if (this.eventPublisher == null) {
+ this.eventPublisher = eventPublisher;
+ }
+ }
+
+ public void unsetEventPublisher(EventPublisher eventPublisher) {
+ if (eventPublisher == this.eventPublisher) {
+ this.eventPublisher = null;
+ }
+ }
+
+}
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java
new file mode 100644
index 00000000000..e5408de0751
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/Expression.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.text;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ResourceBundle;
+
+/**
+ * Base class for all expressions.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+public abstract class Expression {
+
+ Expression() {
+ }
+
+ abstract ASTNode parse(ResourceBundle language, TokenList list);
+
+ void generateValue(ASTNode node) {
+ }
+
+ List getChildExpressions() {
+ return Collections.emptyList();
+ }
+
+ abstract boolean collectFirsts(ResourceBundle language, HashSet firsts);
+
+ HashSet getFirsts(ResourceBundle language) {
+ HashSet firsts = new HashSet();
+ collectFirsts(language, firsts);
+ return firsts;
+ }
+}
\ No newline at end of file
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java
new file mode 100644
index 00000000000..17348fda874
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionAlternatives.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.text;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ResourceBundle;
+
+/**
+ * Expression that successfully parses, if one of the given alternative expressions matches. This class is immutable.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+final class ExpressionAlternatives extends Expression {
+
+ private List subExpressions;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param subExpressions the sub expressions that are tried/parsed as alternatives in the given order
+ */
+ public ExpressionAlternatives(Expression... subExpressions) {
+ super();
+ this.subExpressions = Collections
+ .unmodifiableList(Arrays.asList(Arrays.copyOf(subExpressions, subExpressions.length)));
+ }
+
+ @Override
+ ASTNode parse(ResourceBundle language, TokenList list) {
+ ASTNode node = new ASTNode(), cr;
+ for (int i = 0; i < subExpressions.size(); i++) {
+ cr = subExpressions.get(i).parse(language, list);
+ if (cr.isSuccess()) {
+ node.setChildren(new ASTNode[] { cr });
+ node.setRemainingTokens(cr.getRemainingTokens());
+ node.setSuccess(true);
+ node.setValue(cr.getValue());
+ generateValue(node);
+ return node;
+ }
+ }
+ return node;
+ }
+
+ @Override
+ List getChildExpressions() {
+ return subExpressions;
+ }
+
+ @Override
+ boolean collectFirsts(ResourceBundle language, HashSet firsts) {
+ boolean blocking = true;
+ for (Expression e : subExpressions) {
+ blocking = blocking && e.collectFirsts(language, firsts);
+ }
+ return blocking;
+ }
+
+ @Override
+ public String toString() {
+ String s = null;
+ for (Expression e : subExpressions) {
+ s = s == null ? e.toString() : (s + ", " + e.toString());
+ }
+ return "alt(" + s + ")";
+ }
+}
\ No newline at end of file
diff --git a/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java
new file mode 100644
index 00000000000..ce19ab0d86c
--- /dev/null
+++ b/bundles/core/org.eclipse.smarthome.core.voice/src/main/java/org/eclipse/smarthome/core/voice/text/ExpressionCardinality.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2014-2016 by the respective copyright holders.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.eclipse.smarthome.core.voice.text;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ResourceBundle;
+
+/**
+ * Expression that successfully parses, if a given expression occurs or repeats with a specified cardinality. This class
+ * is immutable.
+ *
+ * @author Tilman Kamp - Initial contribution and API
+ *
+ */
+public final class ExpressionCardinality extends Expression {
+
+ private Expression subExpression;
+ private boolean atLeastOne = false;
+ private boolean atMostOne = true;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @param subExpression expression that could occur or repeat
+ * @param atLeastOne true, if expression should occur at least one time
+ * @param atMostOne true, if expression should occur at most one time
+ */
+ public ExpressionCardinality(Expression subExpression, boolean atLeastOne, boolean atMostOne) {
+ this.subExpression = subExpression;
+ this.atLeastOne = atLeastOne;
+ this.atMostOne = atMostOne;
+ }
+
+ @Override
+ ASTNode parse(ResourceBundle language, TokenList list) {
+ ASTNode node = new ASTNode(), cr;
+ ArrayList nodes = new ArrayList();
+ ArrayList