diff --git a/CODEOWNERS b/CODEOWNERS
index c09e2b9f1e3be..bcba505f25025 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -197,6 +197,7 @@
/bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff @antroids
/bundles/org.openhab.binding.mqtt.homie/ @davidgraeff
/bundles/org.openhab.binding.myq/ @digitaldan
+/bundles/org.openhab.binding.mycroft/ @dalgwen
/bundles/org.openhab.binding.mystrom/ @pail23
/bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn
/bundles/org.openhab.binding.neato/ @jjlauterbach
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 8e2a9ade62de9..7c8e528bd4eef 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -971,6 +971,11 @@
org.openhab.binding.mqtt.homie${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.mycroft
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.myq
diff --git a/bundles/org.openhab.binding.mycroft/NOTICE b/bundles/org.openhab.binding.mycroft/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.mycroft/README.md b/bundles/org.openhab.binding.mycroft/README.md
new file mode 100644
index 0000000000000..5ad2007c9c2cd
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/README.md
@@ -0,0 +1,128 @@
+# Mycroft Binding
+
+This binding connects to Mycroft A.I. in order to control it or react to events by listening on the message bus.
+
+Possibilies include:
+
+- Press a button in openHAB to wake Mycroft without using a wake word.
+- Simulate a voice command to launch a skill, as if you just spoke it
+- Send some text that Mycroft will say (Using its Text To Speech service)
+- Control the music player
+- Mute the sound volume of Mycroft
+- React to all the aforementioned events ...
+- ... and send/receive any other kind of messages on the message bus
+
+
+## Supported Things
+
+The only thing managed by this binding is a Mycroft instance
+
+| Thing Type ID | Description |
+|--------------------|----------------------------------------------------------------------------|
+| mycroft | A Mark I/II, a Picroft, or any other variant exposing the message bus |
+
+
+
+## Discovery
+
+There is no discovery service, as Mycroft doesn't announce itself on the network.
+
+
+## Thing Configuration
+
+The configuration is simple, as you just need to give the IP/hostname of the Mycroft instance accessible on the network.
+The default port is 8181, which can be changed.
+
+```
+Thing mycroft:mycroft:myMycroft "Mycroft A.I." @ "Living Room" [host="192.168.X.X"]
+```
+
+| property | type | description | mandatory |
+|--------------------------|------------------------|------------------------------------------------------------------|-----------|
+| host | IP or string | IP address or hostname | Yes |
+| port | integer | Port to reach Mycroft (default 8181) | No |
+| volume_restoration_level | integer | When unmuted, force Mycroft to restore volume to this value | No |
+
+
+## Channels
+
+A Mycroft thing has the following channels:
+
+
+| channel type id | Item type | description |
+|------------------------------|-----------|------------------------------------------------------------------------------------------------|
+| listen | Switch | Switch to ON when Mycroft is listening. Can simulate a wake word detection to trigger the STT |
+| speak | String | The last sentence Mycroft speaks |
+| utterance | String | The last utterance Mycroft receive |
+| player | Player | The music player Mycroft is currently controlling |
+| volume_mute | Switch | Mute the Mycroft speaker |
+| volume | Dimmer | The volume of the Mycroft speaker. (Note : Value unreliable until a volume change occured) |
+| full_message | String | The last message (full json) seen on the Mycroft Bus. Filtered by the messageTypes properties |
+
+
+The channel 'full_message' has the following configuration available:
+
+| property | type | description | mandatory |
+|---------------|---------------------------------|-------------------------------------------------------------------------|-----------|
+| messageTypes | List of string, comma separated | Only these message types will be forwarded to the Full Message Channel | No |
+
+
+## Full Example
+
+A manual setup through a `things/mycroft.things` file could look like this:
+
+```java
+Thing mycroft:mycroft:myMycroft "Mycroft A.I." @ "Living Room" [host="192.168.X.X", port=8181] {
+ Channels:
+ Type full-message-channel : Text [
+ messageTypes="message.type.1,message.type.4"
+ ]
+}
+```
+
+### Item Configuration
+
+The `mycroft.item` file:
+
+```java
+Switch myMycroft_mute "Mute" { channel="mycroft:mycroft:myMycroft:volume_mute" }
+Dimmer myMycroft_volume "Volume [%d]" { channel="mycroft:mycroft:myMycroft:volume" }
+Player myMycroft_player "Control" { channel="mycroft:mycroft:myMycroft:player" }
+Switch myMycroft_listen "Wake and listen" { channel="mycroft:mycroft:myMycroft:listen" }
+String myMycroft_speak "Speak STT" { channel="mycroft:mycroft:myMycroft:speak" }
+String myMycroft_utterance "Utterance" { channel="mycroft:mycroft:myMycroft:utterance" }
+String myMycroft_fullmessage "Full JSON message" { channel="mycroft:mycroft:myMycroft:full_message" }
+```
+
+### Sitemap Configuration
+
+A `demo.sitemap` file:
+
+```
+sitemap demo label="myMycroft"
+{
+ Frame label="myMycroft" {
+ Switch item=myMycroft_mute
+ Slider item=myMycroft_volume
+ Default item=myMycroft_player
+ Switch item=myMycroft_listen
+ Text item=myMycroft_speak
+ Text item=myMycroft_utterance
+ Text item=myMycroft_fullmessage
+ }
+}
+```
+
+
+### Ask Mycroft to say something
+
+mycroft.rules
+
+```java
+rule "Say Hello"
+when
+ Item Presence_Isaac changed
+then
+ myMycroft_speak.sendCommand("Hello Isaac")
+end
+```
diff --git a/bundles/org.openhab.binding.mycroft/pom.xml b/bundles/org.openhab.binding.mycroft/pom.xml
new file mode 100644
index 0000000000000..c64f7f086a7c9
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.3.0-SNAPSHOT
+
+
+ org.openhab.binding.mycroft
+
+ openHAB Add-ons :: Bundles :: Mycroft Binding
+
+
diff --git a/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml b/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..04d5e3b8ff972
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml
@@ -0,0 +1,10 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ openhab-transport-http
+ mvn:org.openhab.addons.bundles/org.openhab.binding.mycroft/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java
new file mode 100644
index 0000000000000..cec1aabacfb58
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link MycroftBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class MycroftBindingConstants {
+
+ private static final String BINDING_ID = "mycroft";
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID MYCROFT = new ThingTypeUID(BINDING_ID, "mycroft");
+
+ // List of all Channel ids
+ public static final String LISTEN_CHANNEL = "listen";
+ public static final String SPEAK_CHANNEL = "speak";
+ public static final String PLAYER_CHANNEL = "player";
+ public static final String VOLUME_CHANNEL = "volume";
+ public static final String VOLUME_MUTE_CHANNEL = "volume_mute";
+ public static final String UTTERANCE_CHANNEL = "utterance";
+ public static final String FULL_MESSAGE_CHANNEL = "full_message";
+
+ // Channel property :
+ public static final String FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY = "messageTypes";
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java
new file mode 100644
index 0000000000000..efe76fc85db76
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link MycroftConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class MycroftConfiguration {
+
+ public String host = "";
+ public int port = 8181;
+ public int volume_restoration_level = 0;
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java
new file mode 100644
index 0000000000000..d086274dc88c5
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.MycroftConnection;
+import org.openhab.binding.mycroft.internal.api.MycroftConnectionListener;
+import org.openhab.binding.mycroft.internal.api.MycroftMessageListener;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
+import org.openhab.binding.mycroft.internal.channels.AudioPlayerChannel;
+import org.openhab.binding.mycroft.internal.channels.ChannelCommandHandler;
+import org.openhab.binding.mycroft.internal.channels.FullMessageChannel;
+import org.openhab.binding.mycroft.internal.channels.ListenChannel;
+import org.openhab.binding.mycroft.internal.channels.MuteChannel;
+import org.openhab.binding.mycroft.internal.channels.MycroftChannel;
+import org.openhab.binding.mycroft.internal.channels.SpeakChannel;
+import org.openhab.binding.mycroft.internal.channels.UtteranceChannel;
+import org.openhab.binding.mycroft.internal.channels.VolumeChannel;
+import org.openhab.core.io.net.http.WebSocketFactory;
+import org.openhab.core.thing.Channel;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingStatus;
+import org.openhab.core.thing.ThingStatusDetail;
+import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link MycroftHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class MycroftHandler extends BaseThingHandler implements MycroftConnectionListener {
+
+ private final Logger logger = LoggerFactory.getLogger(MycroftHandler.class);
+
+ private final WebSocketFactory webSocketFactory;
+ private @NonNullByDefault({}) MycroftConnection connection;
+ private @Nullable ScheduledFuture> scheduledFuture;
+ private MycroftConfiguration config = new MycroftConfiguration();
+ private boolean thingDisposing = false;
+ protected Map> mycroftChannels = new HashMap<>();
+
+ /** The reconnect frequency in case of error */
+ private static final int POLL_FREQUENCY_SEC = 30;
+ private int sometimesSendVolumeRequest = 0;
+
+ public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) {
+ super(thing);
+ this.webSocketFactory = webSocketFactory;
+ }
+
+ /**
+ * Stops the API request or websocket reconnect timer
+ */
+ private void stopTimer() {
+ ScheduledFuture> future = scheduledFuture;
+ if (future != null) {
+ future.cancel(false);
+ scheduledFuture = null;
+ }
+ }
+
+ /**
+ * Starts the websocket connection.
+ * It sometimes also sends a get volume request to check the connection and refresh the volume.
+ */
+ private void checkOrstartWebsocket() {
+ if (thingDisposing) {
+ return;
+ }
+ if (connection.isConnected()) {
+ // sometimes test the connection by sending a real message
+ // AND refreshing volume in the same step
+ if (sometimesSendVolumeRequest >= 3) { // arbitrary one on three times
+ sometimesSendVolumeRequest = 0;
+ sendMessage(new MessageVolumeGet());
+ } else {
+ sometimesSendVolumeRequest++;
+ }
+ } else {
+ connection.start(config.host, config.port);
+ }
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ ChannelCommandHandler channelCommand = mycroftChannels.get(channelUID);
+ if (channelCommand == null) {
+ logger.error("Command {} for channel {} cannot be handled", command.toString(), channelUID.toString());
+ } else {
+ channelCommand.handleCommand(command);
+ }
+ }
+
+ @Override
+ public void initialize() {
+ thingDisposing = false;
+
+ updateStatus(ThingStatus.UNKNOWN);
+
+ logger.debug("Start initializing Mycroft {}", thing.getUID());
+
+ String websocketID = thing.getUID().getAsString().replace(':', '-');
+ if (websocketID.length() < 4) {
+ websocketID = "mycroft-" + websocketID;
+ }
+ if (websocketID.length() > 20) {
+ websocketID = websocketID.substring(websocketID.length() - 20);
+ }
+ this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID));
+
+ config = getConfigAs(MycroftConfiguration.class);
+ if (config.host.isBlank()) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "No host defined");
+ return;
+ } else if (config.port < 0 || config.port > 0xFFFF) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
+ "Port should be between 0 and 65536");
+ return;
+ }
+ scheduledFuture = scheduler.scheduleWithFixedDelay(this::checkOrstartWebsocket, 0, POLL_FREQUENCY_SEC,
+ TimeUnit.SECONDS);
+
+ registerChannel(new ListenChannel(this));
+ registerChannel(new VolumeChannel(this));
+ registerChannel(new MuteChannel(this, config.volume_restoration_level));
+ registerChannel(new SpeakChannel(this));
+ registerChannel(new AudioPlayerChannel(this));
+ registerChannel(new UtteranceChannel(this));
+
+ final Channel fullMessageChannel = getThing().getChannel(MycroftBindingConstants.FULL_MESSAGE_CHANNEL);
+ @SuppressWarnings("null") // cannot be null
+ String messageTypesProperty = (String) fullMessageChannel.getConfiguration()
+ .get(MycroftBindingConstants.FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY);
+
+ registerChannel(new FullMessageChannel(this, messageTypesProperty));
+
+ checkLinkedChannelsAndRegisterMessageListeners();
+ }
+
+ private void checkLinkedChannelsAndRegisterMessageListeners() {
+ for (Entry> channelEntry : mycroftChannels.entrySet()) {
+ ChannelUID uid = channelEntry.getKey();
+ MycroftChannel> channel = channelEntry.getValue();
+ if (isLinked(uid)) {
+ channel.registerListeners();
+ } else {
+ channel.unregisterListeners();
+ }
+ }
+ }
+
+ @Override
+ public void channelLinked(ChannelUID channelUID) {
+ checkLinkedChannelsAndRegisterMessageListeners();
+ }
+
+ @Override
+ public void channelUnlinked(ChannelUID channelUID) {
+ checkLinkedChannelsAndRegisterMessageListeners();
+ }
+
+ private void registerChannel(MycroftChannel> channel) {
+ mycroftChannels.put(channel.getChannelUID(), channel);
+ }
+
+ public void registerMessageListener(MessageType messageType, MycroftMessageListener> listener) {
+ this.connection.registerListener(messageType, listener);
+ }
+
+ public void unregisterMessageListener(MessageType messageType, MycroftMessageListener> listener) {
+ this.connection.unregisterListener(messageType, listener);
+ }
+
+ @Override
+ public void connectionEstablished() {
+ logger.debug("Mycroft thing {} is online", thing.getUID());
+ updateStatus(ThingStatus.ONLINE);
+ }
+
+ @Override
+ public void connectionLost(String reason) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
+ }
+
+ @Override
+ public void dispose() {
+ thingDisposing = true;
+ stopTimer();
+ connection.close();
+ }
+
+ public void updateMyChannel(MycroftChannel mycroftChannel, T state) {
+ updateState(mycroftChannel.getChannelUID(), state);
+ }
+
+ public boolean sendMessage(BaseMessage message) {
+ try {
+ connection.sendMessage(message);
+ return true;
+ } catch (IOException e) {
+ logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
+ return false;
+ }
+ }
+
+ public boolean sendMessage(String message) {
+ try {
+ connection.sendMessage(message);
+ return true;
+ } catch (IOException e) {
+ logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
+ return false;
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java
new file mode 100644
index 0000000000000..a403fb8a23add
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+package org.openhab.binding.mycroft.internal;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.io.net.http.WebSocketFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link MycroftHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.mycroft", service = ThingHandlerFactory.class)
+public class MycroftHandlerFactory extends BaseThingHandlerFactory {
+
+ private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(MycroftBindingConstants.MYCROFT);
+
+ private final WebSocketFactory webSocketFactory;
+
+ @Activate
+ public MycroftHandlerFactory(final @Reference WebSocketFactory webSocketFactory) {
+ this.webSocketFactory = webSocketFactory;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ ThingTypeUID thingTypeUID = thing.getThingTypeUID();
+
+ if (MycroftBindingConstants.MYCROFT.equals(thingTypeUID)) {
+ return new MycroftHandler(thing, webSocketFactory);
+ }
+
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java
new file mode 100644
index 0000000000000..29320d95bcff0
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import java.util.stream.Stream;
+
+import javax.validation.constraints.NotNull;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioNext;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPause;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPlay;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPrev;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioResume;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioStop;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfo;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfoReply;
+import org.openhab.binding.mycroft.internal.api.dto.MessageMicListen;
+import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordBegin;
+import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordEnd;
+import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopUtterance;
+import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDecrease;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDuck;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGetResponse;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeIncrease;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeMute;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnduck;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnmute;
+
+/**
+ * All message type of interest, issued by Mycroft, are referenced here
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public enum MessageType {
+
+ any("special-anymessages", BaseMessage.class),
+ speak("speak", MessageSpeak.class),
+ recognizer_loop__record_begin("recognizer_loop:record_begin", MessageRecognizerLoopRecordBegin.class),
+ recognizer_loop__record_end("recognizer_loop:record_end", MessageRecognizerLoopRecordEnd.class),
+ recognizer_loop__utterance("recognizer_loop:utterance", MessageRecognizerLoopUtterance.class),
+ mycroft_mic_listen("mycroft.mic.listen", MessageMicListen.class),
+
+ mycroft_audio_service_pause("mycroft.audio.service.pause", MessageAudioPause.class),
+ mycroft_audio_service_resume("mycroft.audio.service.resume", MessageAudioResume.class),
+ mycroft_audio_service_stop("mycroft.audio.service.stop", MessageAudioStop.class),
+ mycroft_audio_service_play("mycroft.audio.service.play", MessageAudioPlay.class),
+ mycroft_audio_service_next("mycroft.audio.service.next", MessageAudioNext.class),
+ mycroft_audio_service_prev("mycroft.audio.service.prev", MessageAudioPrev.class),
+ mycroft_audio_service_track_info("mycroft.audio.service.track_info", MessageAudioTrackInfo.class),
+ mycroft_audio_service_track_info_reply("mycroft.audio.service.track_info_reply", MessageAudioTrackInfoReply.class),
+
+ mycroft_volume_set("mycroft.volume.set", MessageVolumeSet.class),
+ mycroft_volume_increase("mycroft.volume.increase", MessageVolumeIncrease.class),
+ mycroft_volume_decrease("mycroft.volume.decrease", MessageVolumeDecrease.class),
+ mycroft_volume_get("mycroft.volume.get", MessageVolumeGet.class),
+ mycroft_volume_get_response("mycroft.volume.get.response", MessageVolumeGetResponse.class),
+ mycroft_volume_mute("mycroft.volume.mute", MessageVolumeMute.class),
+ mycroft_volume_unmute("mycroft.volume.unmute", MessageVolumeUnmute.class),
+ mycroft_volume_duck("mycroft.volume.duck", MessageVolumeDuck.class),
+ mycroft_volume_unduck("mycroft.volume.unduck", MessageVolumeUnduck.class),
+
+ mycroft_reminder_mycroftai__reminder("mycroft-reminder.mycroftai:reminder", BaseMessage.class),
+ mycroft_date_time_mycroftai__timeskillupdate_display("mycroft-date-time.mycroftai:TimeSkillupdate_display",
+ BaseMessage.class),
+ mycroft_configuration_mycroftai__configurationskillupdate_remote(
+ "mycroft-configuration.mycroftai:ConfigurationSkillupdate_remote", BaseMessage.class);
+
+ private @NotNull Class extends BaseMessage> messageTypeClass;
+ private @NotNull String messageTypeName;
+
+ MessageType(String messageTypeName, Class extends BaseMessage> messageType) {
+ this.messageTypeClass = messageType;
+ this.messageTypeName = messageTypeName;
+ }
+
+ /**
+ * Get the expected message type for this message
+ *
+ * @return The message type class associated with this type
+ */
+ public @NotNull Class extends BaseMessage> getMessageTypeClass() {
+ return messageTypeClass;
+ }
+
+ @NotNull
+ public static MessageType fromString(String asString) {
+ return Stream.of(values()).filter(messageType -> messageType.messageTypeName.equals(asString)).findFirst()
+ .orElse(any);
+ }
+
+ public String getMessageTypeName() {
+ return messageTypeName;
+ }
+
+ protected void setMessageTypeName(String messageTypeName) {
+ this.messageTypeName = messageTypeName;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java
new file mode 100644
index 0000000000000..4c476b94b6fff
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+/**
+ * Custom deserializer
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class MessageTypeConverter implements JsonDeserializer, JsonSerializer {
+ @Override
+ public @Nullable MessageType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
+ throws JsonParseException {
+ MessageType messageType = MessageType.fromString(json.getAsString());
+ // for message of type non recognized :
+ messageType.setMessageTypeName(json.getAsString());
+ return messageType;
+ }
+
+ @Override
+ public JsonElement serialize(MessageType src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(src.getMessageTypeName());
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java
new file mode 100644
index 0000000000000..d7fbc623a5226
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java
@@ -0,0 +1,255 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
+import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
+import org.eclipse.jetty.websocket.api.annotations.WebSocket;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+/**
+ * Establishes and keeps a websocket connection to the Mycroft bus
+ *
+ * @author Gwendal Roulleau - Initial contribution. Inspired by the deconz binding.
+ */
+@WebSocket
+@NonNullByDefault
+public class MycroftConnection {
+ private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger();
+ private final Logger logger = LoggerFactory.getLogger(MycroftConnection.class);
+
+ private final WebSocketClient client;
+ private final String socketName;
+ private final Gson gson;
+
+ private final MycroftConnectionListener connectionListener;
+ private final Map>> listeners = new ConcurrentHashMap<>();
+
+ private ConnectionState connectionState = ConnectionState.DISCONNECTED;
+ private @Nullable Session session;
+
+ private static final int TIMEOUT_MILLISECONDS = 3000;
+
+ public MycroftConnection(MycroftConnectionListener listener, WebSocketClient client) {
+ this.connectionListener = listener;
+ this.client = client;
+ this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
+ this.client.setMaxIdleTimeout(0);
+ this.socketName = "Websocket-Mycroft$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
+
+ GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeAdapter(MessageType.class, new MessageTypeConverter());
+ gson = gsonBuilder.create();
+ }
+
+ public MycroftConnection(MycroftConnectionListener listener) {
+ this(listener, new WebSocketClient());
+ }
+
+ public void start(String ip, int port) {
+ if (connectionState == ConnectionState.CONNECTED) {
+ return;
+ } else if (connectionState == ConnectionState.CONNECTING) {
+ logger.debug("{} already connecting", socketName);
+ return;
+ } else if (connectionState == ConnectionState.DISCONNECTING) {
+ logger.warn("{} trying to re-connect while still disconnecting", socketName);
+ }
+ Future futureConnect = null;
+ try {
+ URI destUri = URI.create("ws://" + ip + ":" + port + "/core");
+ client.start();
+ logger.debug("Trying to connect {} to {}", socketName, destUri);
+ futureConnect = client.connect(this, destUri);
+ futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ if (futureConnect != null) {
+ futureConnect.cancel(true);
+ }
+ connectionListener
+ .connectionLost("Error while connecting: " + (e.getMessage() != null ? e.getMessage() : "unknown"));
+ }
+ }
+
+ public void close() {
+ try {
+ connectionState = ConnectionState.DISCONNECTING;
+ client.stop();
+ } catch (Exception e) {
+ logger.debug("{} encountered an error while closing connection", socketName, e);
+ }
+ client.destroy();
+ }
+
+ /**
+ * The listener registered in this method will be called when a corresponding message will be detected
+ * on the Mycroft bus.
+ *
+ * @param messageType The message type to listen to.
+ * @param listener The listener will receive a callback when the requested message type will be detected on the bus.
+ */
+ public void registerListener(MessageType messageType, MycroftMessageListener extends BaseMessage> listener) {
+ Set> messageTypeListeners = listeners.get(messageType);
+ if (messageTypeListeners == null) {
+ messageTypeListeners = new HashSet>();
+ listeners.put(messageType, messageTypeListeners);
+ }
+ messageTypeListeners.add(listener);
+ }
+
+ public void unregisterListener(MessageType messageType, MycroftMessageListener> listener) {
+ Optional.ofNullable(listeners.get(messageType))
+ .ifPresent((messageTypeListeners) -> messageTypeListeners.remove(listener));
+ }
+
+ public void sendMessage(BaseMessage message) throws IOException {
+ sendMessage(gson.toJson(message));
+ }
+
+ public void sendMessage(String message) throws IOException {
+ final Session storedSession = this.session;
+ try {
+ if (storedSession != null) {
+ storedSession.getRemote().sendString(message);
+ } else {
+ throw new IOException("Session is not initialized");
+ }
+ } catch (IOException e) {
+ if (storedSession != null && storedSession.isOpen()) {
+ storedSession.close(-1, "Sending message error");
+ }
+ throw e;
+ }
+ }
+
+ @OnWebSocketConnect
+ public void onConnect(Session session) {
+ connectionState = ConnectionState.CONNECTED;
+ logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
+ session.hashCode());
+ connectionListener.connectionEstablished();
+ this.session = session;
+ }
+
+ @OnWebSocketMessage
+ public void onMessage(Session session, String message) {
+ if (!session.equals(this.session)) {
+ handleWrongSession(session, message);
+ return;
+ }
+ logger.trace("{} received raw data: {}", socketName, message);
+
+ try {
+ // get the base message information :
+ BaseMessage mycroftMessage = gson.fromJson(message, BaseMessage.class);
+ Objects.requireNonNull(mycroftMessage);
+ // now that we have the message type, we can use a second and more precise parsing:
+ if (mycroftMessage.type != MessageType.any) {
+ mycroftMessage = gson.fromJson(message, mycroftMessage.type.getMessageTypeClass());
+ Objects.requireNonNull(mycroftMessage);
+ }
+ // adding the raw message:
+ mycroftMessage.message = message;
+
+ final BaseMessage finalMessage = mycroftMessage;
+ Stream.concat(listeners.getOrDefault(MessageType.any, new HashSet<>()).stream(),
+ listeners.getOrDefault(mycroftMessage.type, new HashSet<>()).stream()).forEach(listener -> {
+ listener.baseMessageReceived(finalMessage);
+ });
+
+ } catch (RuntimeException e) {
+ // we need to catch all processing exceptions, otherwise they could affect the connection
+ logger.debug("{} encountered an error while processing the message {}: {}", socketName, message,
+ e.getMessage());
+ }
+ }
+
+ @OnWebSocketError
+ public void onError(@Nullable Session session, Throwable cause) {
+
+ if (session == null || !session.equals(this.session)) {
+ handleWrongSession(session, "Connection error: " + cause.getMessage());
+ return;
+ }
+ logger.debug("{} connection error, closing: {}", socketName, cause.getMessage());
+
+ Session storedSession = this.session;
+ if (storedSession != null && storedSession.isOpen()) {
+ storedSession.close(-1, "Processing error");
+ }
+ }
+
+ @OnWebSocketClose
+ public void onClose(Session session, int statusCode, String reason) {
+ if (!session.equals(this.session)) {
+ handleWrongSession(session, "Connection closed: " + statusCode + " / " + reason);
+ return;
+ }
+ logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
+ connectionState = ConnectionState.DISCONNECTED;
+ this.session = null;
+ connectionListener.connectionLost(reason);
+ }
+
+ private void handleWrongSession(@Nullable Session session, String message) {
+ if (session == null) {
+ logger.debug("received and discarded message for null session : {}", message);
+ } else {
+ logger.debug("{} received and discarded message for other session {}: {}.", socketName, session.hashCode(),
+ message);
+ }
+ }
+
+ /**
+ * check connection state (successfully connected)
+ *
+ * @return true if connected, false if connecting, disconnecting or disconnected
+ */
+ public boolean isConnected() {
+ return connectionState == ConnectionState.CONNECTED;
+ }
+
+ /**
+ * used internally to represent the connection state
+ */
+ private enum ConnectionState {
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTING,
+ DISCONNECTED
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java
new file mode 100644
index 0000000000000..6990de076a792
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Informs about the websocket connection.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public interface MycroftConnectionListener {
+ /**
+ * Connection successfully established.
+ */
+ void connectionEstablished();
+
+ /**
+ * Connection lost. A reconnect timer has been started.
+ *
+ * @param reason A reason for the disconnection
+ */
+ void connectionLost(String reason);
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java
new file mode 100644
index 0000000000000..86a8ca79a7f83
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+
+/**
+ * Informs about received messages
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public interface MycroftMessageListener {
+ /**
+ * A new message was received
+ *
+ * @param message The received message
+ */
+ void messageReceived(T message);
+
+ @SuppressWarnings("unchecked")
+ default void baseMessageReceived(BaseMessage baseMessage) {
+ try {
+ messageReceived(((T) baseMessage));
+ } catch (ClassCastException cce) {
+ throw new ClassCastException("Incorrect use of message in Mycroft binding");
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java
new file mode 100644
index 0000000000000..94b366e3c2cf4
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This is the base message class for all messages circulating on the Mycroft bus.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class BaseMessage {
+
+ public MessageType type;
+ public String message = "";
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java
new file mode 100644
index 0000000000000..5bef6536ea74a
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to play the next title in
+ * its underlying player.
+ *
+ * @author Gwendal Roulleau - Initial Contribution
+ *
+ */
+public class MessageAudioNext extends BaseMessage {
+
+ public MessageAudioNext() {
+ this.type = MessageType.mycroft_audio_service_next;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java
new file mode 100644
index 0000000000000..6775e8d948e11
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to pause
+ * its underlying player.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageAudioPause extends BaseMessage {
+
+ public MessageAudioPause() {
+ this.type = MessageType.mycroft_audio_service_pause;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java
new file mode 100644
index 0000000000000..3fd2f360d96c3
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to send the play command to
+ * its underlying player.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+public class MessageAudioPlay extends BaseMessage {
+
+ public MessageAudioPlay() {
+ this.type = MessageType.mycroft_audio_service_play;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java
new file mode 100644
index 0000000000000..c3df5061dca6c
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to play the previous title in
+ * its underlying player.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+public class MessageAudioPrev extends BaseMessage {
+
+ public MessageAudioPrev() {
+ this.type = MessageType.mycroft_audio_service_prev;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java
new file mode 100644
index 0000000000000..6db55a43e9a0d
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to send resume command to
+ * its underlying player
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+public class MessageAudioResume extends BaseMessage {
+
+ public MessageAudioResume() {
+ this.type = MessageType.mycroft_audio_service_resume;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java
new file mode 100644
index 0000000000000..9b989670bb9a1
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to stop
+ * its underlying player.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageAudioStop extends BaseMessage {
+
+ public MessageAudioStop() {
+ this.type = MessageType.mycroft_audio_service_stop;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java
new file mode 100644
index 0000000000000..91ae000d8b7c3
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to give information about
+ * the title played on its underlying player.
+ * Work in progress
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageAudioTrackInfo extends BaseMessage {
+
+ public MessageAudioTrackInfo() {
+ this.type = MessageType.mycroft_audio_service_track_info;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java
new file mode 100644
index 0000000000000..71055a6ed757f
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent by Mycroft to give information about
+ * the title played on its underlying player.
+ * Work in progress
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageAudioTrackInfoReply extends BaseMessage {
+
+ public MessageAudioTrackInfoReply() {
+ this.type = MessageType.mycroft_audio_service_track_info_reply;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java
new file mode 100644
index 0000000000000..26f130a096eab
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to begin to listen to the mic
+ * and to try to do STT and intent recognition.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageMicListen extends BaseMessage {
+
+ public MessageMicListen() {
+ this.type = MessageType.mycroft_mic_listen;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java
new file mode 100644
index 0000000000000..b31104de5d648
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message informs the bus clients that Mycroft
+ * is actively listening and trying to do STT.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageRecognizerLoopRecordBegin extends BaseMessage {
+
+ public Context context = new Context();
+
+ public MessageRecognizerLoopRecordBegin() {
+ this.type = MessageType.recognizer_loop__record_begin;
+ }
+
+ public static class Context {
+ public String client_name = "";
+ public String source = "";
+ public String destination = "";
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java
new file mode 100644
index 0000000000000..c00ec55e02d24
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message informs the bus clients that Mycroft
+ * finished listening to the mic.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageRecognizerLoopRecordEnd extends BaseMessage {
+
+ public Context context = new Context();
+
+ public MessageRecognizerLoopRecordEnd() {
+ this.type = MessageType.recognizer_loop__record_end;
+ }
+
+ public static class Context {
+ public String client_name = "";
+ public String source = "";
+ public String destination = "";
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java
new file mode 100644
index 0000000000000..a8fb2a359b0aa
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent to the skills
+ * module to trigger an intent from a text.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageRecognizerLoopUtterance extends BaseMessage {
+
+ public Data data = new Data();
+
+ public Context context = new Context();
+
+ public MessageRecognizerLoopUtterance() {
+ this.type = MessageType.recognizer_loop__utterance;
+ }
+
+ public MessageRecognizerLoopUtterance(String utterance) {
+ this();
+ this.data.utterances.add(utterance);
+ this.context.client_name = "java_api";
+ this.context.source = "audio";
+ this.context.destination.add("skills");
+ }
+
+ public static class Data {
+ public List utterances = new ArrayList<>();
+ }
+
+ public static class Context {
+ public String client_name = "";
+ public String source = "";
+ public List destination = new ArrayList<>();
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java
new file mode 100644
index 0000000000000..1f34a46f68aae
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent to the Mycroft audio module
+ * to trigger a TTS action.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageSpeak extends BaseMessage {
+
+ public Data data = new Data();
+
+ public Context context = new Context();
+
+ public MessageSpeak() {
+ this.type = MessageType.speak;
+ }
+
+ public MessageSpeak(String textToSay) {
+ this();
+ this.data = new Data();
+ this.data.utterance = textToSay;
+ }
+
+ public static class Data {
+ public String utterance = "";
+ public String expect_response = "";
+ };
+
+ public static class Context {
+ public String client_name = "";
+ public List source = new ArrayList<>();
+ public String destination = "";
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java
new file mode 100644
index 0000000000000..15281a860c40c
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to decrease the volume by 10%
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeDecrease extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeDecrease() {
+ this.type = MessageType.mycroft_volume_decrease;
+ }
+
+ public static class Data {
+ public Boolean play_sound = true;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java
new file mode 100644
index 0000000000000..42cc439f8c0d6
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent by Mycroft to signal that the volume
+ * is ducked during a STT recognition process.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeDuck extends BaseMessage {
+
+ public Data data = new Data();
+ public Context context = new Context();
+
+ public MessageVolumeDuck() {
+ this.type = MessageType.mycroft_volume_duck;
+ }
+
+ public static final class Data {
+ }
+
+ public static final class Context {
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java
new file mode 100644
index 0000000000000..d62e0ddaf79fa
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to answer with the current volume
+ * NOT FUNCTIONAL
+ * (see https://community.mycroft.ai/t/openhab-plugin-development-audio-volume-message-types-missing/10576)
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeGet extends BaseMessage {
+
+ public Data data = new Data();
+ public Context context = new Context();
+
+ public MessageVolumeGet() {
+ this.type = MessageType.mycroft_volume_get;
+ }
+
+ public static final class Data {
+ }
+
+ public static final class Context {
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java
new file mode 100644
index 0000000000000..cbbbbb992a646
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent in response to a VolumeGet message
+ * with the current volume in Mycroft
+ * NOT FUNCTIONAL
+ * (see https://community.mycroft.ai/t/openhab-plugin-development-audio-volume-message-types-missing/10576)
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeGetResponse extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeGetResponse() {
+ this.type = MessageType.mycroft_volume_get_response;
+ }
+
+ public static class Data {
+ public float percent = 0;
+ public Boolean muted = false;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java
new file mode 100644
index 0000000000000..48b1a47c0422a
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to increase the volume by 10%
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeIncrease extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeIncrease() {
+ this.type = MessageType.mycroft_volume_increase;
+ }
+
+ public static class Data {
+ public Boolean play_sound = true;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java
new file mode 100644
index 0000000000000..b35b674f98eef
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to mute the volume
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeMute extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeMute() {
+ this.type = MessageType.mycroft_volume_mute;
+ }
+
+ public static class Data {
+ public Boolean speak_message = false;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java
new file mode 100644
index 0000000000000..8f4738da4a460
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks IN THEORY Mycroft to set the volume to an amount
+ * specified in the data payload.
+ * But it seems in fact to be a message to inform third party of a
+ * volume change
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeSet extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeSet() {
+ this.type = MessageType.mycroft_volume_set;
+ }
+
+ public static class Data {
+ public float percent = 0;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java
new file mode 100644
index 0000000000000..933d8026b97ca
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message is sent by Mycroft to signal that the volume
+ * is no longer ducked after a STT recognition process.
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeUnduck extends BaseMessage {
+
+ public Data data = new Data();
+ public Context context = new Context();
+
+ public MessageVolumeUnduck() {
+ this.type = MessageType.mycroft_volume_unduck;
+ }
+
+ public static final class Data {
+ }
+
+ public static final class Context {
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java
new file mode 100644
index 0000000000000..94a3d86a45985
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api.dto;
+
+import org.openhab.binding.mycroft.internal.api.MessageType;
+
+/**
+ * This message asks Mycroft to unmute the volume
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+public class MessageVolumeUnmute extends BaseMessage {
+
+ public Data data = new Data();
+
+ public MessageVolumeUnmute() {
+ this.type = MessageType.mycroft_volume_unmute;
+ }
+
+ public static class Data {
+ public Boolean speak_message = false;
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java
new file mode 100644
index 0000000000000..13fd7e4de2ee4
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioNext;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPause;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPlay;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPrev;
+import org.openhab.binding.mycroft.internal.api.dto.MessageAudioResume;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.State;
+
+/**
+ * This channel handles the Mycroft capability to act as a music player
+ * (depending on common play music skills installed)
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class AudioPlayerChannel extends MycroftChannel {
+
+ public AudioPlayerChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.PLAYER_CHANNEL);
+ }
+
+ @Override
+ protected List getMessageToListenTo() {
+ return Arrays.asList(MessageType.mycroft_audio_service_prev, MessageType.mycroft_audio_service_next,
+ MessageType.mycroft_audio_service_pause, MessageType.mycroft_audio_service_resume,
+ MessageType.mycroft_audio_service_play, MessageType.mycroft_audio_service_stop,
+ MessageType.mycroft_audio_service_track_info, MessageType.mycroft_audio_service_track_info_reply);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ switch (message.type) {
+ case mycroft_audio_service_pause:
+ case mycroft_audio_service_stop:
+ updateMyState(PlayPauseType.PAUSE);
+ break;
+ case mycroft_audio_service_play:
+ case mycroft_audio_service_resume:
+ updateMyState(PlayPauseType.PLAY);
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof PlayPauseType) {
+ if (((PlayPauseType) command) == PlayPauseType.PAUSE) {
+ if (handler.sendMessage(new MessageAudioPause())) {
+ updateMyState(PlayPauseType.PAUSE);
+ }
+ }
+ if (((PlayPauseType) command) == PlayPauseType.PLAY) {
+ handler.sendMessage(new MessageAudioPlay());
+ if (handler.sendMessage(new MessageAudioResume())) {
+ updateMyState(PlayPauseType.PLAY);
+ }
+ }
+ }
+ if (command instanceof NextPreviousType) {
+ if (((NextPreviousType) command) == NextPreviousType.NEXT) {
+ if (handler.sendMessage(new MessageAudioNext())) {
+ updateMyState(PlayPauseType.PLAY);
+ }
+ }
+ if (((NextPreviousType) command) == NextPreviousType.PREVIOUS) {
+ if (handler.sendMessage(new MessageAudioPrev())) {
+ updateMyState(PlayPauseType.PLAY);
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java
new file mode 100644
index 0000000000000..2cba16d02ba73
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.types.Command;
+
+/**
+ * Interface for channel which can handle command
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public interface ChannelCommandHandler {
+
+ public void handleCommand(Command command);
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java
new file mode 100644
index 0000000000000..9a650ecb0bae4
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+
+/**
+ * The channel responsible for sending/receiving raw message
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class FullMessageChannel extends MycroftChannel {
+
+ private List messageTypesList = new ArrayList<>();
+
+ public FullMessageChannel(MycroftHandler handler, String messageTypesList) {
+ super(handler, MycroftBindingConstants.FULL_MESSAGE_CHANNEL);
+ for (String messageType : messageTypesList.split(",")) {
+ this.messageTypesList.add(messageType.trim());
+ }
+ }
+
+ @Override
+ public List getMessageToListenTo() {
+ return Arrays.asList(MessageType.any);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ if (messageTypesList.contains(message.type.getMessageTypeName())) {
+ updateMyState(new StringType(message.message));
+ }
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof StringType) {
+ if (handler.sendMessage(command.toFullString())) {
+ updateMyState(new StringType(command.toFullString()));
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java
new file mode 100644
index 0000000000000..b1264438565ad
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageMicListen;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.Command;
+
+/**
+ * The channel responsible for triggering STT recognition
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class ListenChannel extends MycroftChannel {
+
+ public ListenChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.LISTEN_CHANNEL);
+ }
+
+ @Override
+ public List getMessageToListenTo() {
+ return Arrays.asList(MessageType.recognizer_loop__record_begin, MessageType.recognizer_loop__record_end);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ if (message.type == MessageType.recognizer_loop__record_begin) {
+ updateMyState(OnOffType.ON);
+ } else if (message.type == MessageType.recognizer_loop__record_end) {
+ updateMyState(OnOffType.OFF);
+ }
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof OnOffType) {
+ if (command == OnOffType.ON) {
+ handler.sendMessage(new MessageMicListen());
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java
new file mode 100644
index 0000000000000..2681723f1dac8
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeMute;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnmute;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.types.Command;
+
+/**
+ * The channel responsible for muting the Mycroft speaker
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class MuteChannel extends MycroftChannel {
+
+ private int volumeRestorationLevel;
+
+ public MuteChannel(MycroftHandler handler, int volumeRestorationLevel) {
+ super(handler, MycroftBindingConstants.VOLUME_MUTE_CHANNEL);
+ this.volumeRestorationLevel = volumeRestorationLevel;
+ }
+
+ @Override
+ public List getMessageToListenTo() {
+ // we don't listen to mute/unmute message because duck/unduck seems sufficient
+ // and we don't want to change state twice for the same event
+ // but it should be tested on mark I, as volume is handled differently
+ return Arrays.asList(MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck,
+ MessageType.mycroft_volume_set, MessageType.mycroft_volume_increase);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ switch (message.type) {
+ case mycroft_volume_mute:
+ case mycroft_volume_duck:
+ updateMyState(OnOffType.ON);
+ break;
+ case mycroft_volume_unmute:
+ case mycroft_volume_unduck:
+ case mycroft_volume_increase:
+ updateMyState(OnOffType.OFF);
+ break;
+ case mycroft_volume_set:
+ if (((MessageVolumeSet) message).data.percent > 0) {
+ updateMyState(OnOffType.OFF);
+ }
+ break;
+ default:
+ }
+ }
+
+ private boolean sendVolumeSetMessage(float volume) {
+ String messageToSend = VolumeChannel.VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME",
+ Float.valueOf(volume).toString());
+ return handler.sendMessage(messageToSend);
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof OnOffType) {
+ if (command == OnOffType.ON) {
+ if (handler.sendMessage(new MessageVolumeMute())) {
+ updateMyState(OnOffType.ON);
+ }
+ } else if (command == OnOffType.OFF) {
+ if (handler.sendMessage(new MessageVolumeUnmute())) {
+ updateMyState(OnOffType.OFF);
+ // if configured, we can restore the volume to a fixed amount
+ // usefull as a workaround for the broken Mycroft volume behavior
+ if (volumeRestorationLevel > 0) {
+ // we must wait 100ms for Mycroft to handle the message and
+ // setting old volume before forcing to our value
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ }
+ sendVolumeSetMessage(Float.valueOf(volumeRestorationLevel));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java
new file mode 100644
index 0000000000000..22e5a7fa1eb8c
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.MycroftMessageListener;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.core.thing.ChannelUID;
+import org.openhab.core.types.State;
+
+/**
+ * A helper method for channel handling
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public abstract class MycroftChannel
+ implements ChannelCommandHandler, MycroftMessageListener {
+
+ private ChannelUID channelUID;
+ protected MycroftHandler handler;
+
+ public MycroftChannel(MycroftHandler handler, String channelUIDPart) {
+ this.handler = handler;
+ this.channelUID = new ChannelUID(handler.getThing().getUID(), channelUIDPart);
+ }
+
+ public final ChannelUID getChannelUID() {
+ return channelUID;
+ }
+
+ protected final void updateMyState(T state) {
+ handler.updateMyChannel(this, state);
+ }
+
+ public final void registerListeners() {
+ for (MessageType messageType : getMessageToListenTo()) {
+ handler.registerMessageListener(messageType, this);
+ }
+ }
+
+ protected List getMessageToListenTo() {
+ return new ArrayList<>();
+ }
+
+ public final void unregisterListeners() {
+ for (MessageType messageType : getMessageToListenTo()) {
+ handler.unregisterMessageListener(messageType, this);
+ }
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java
new file mode 100644
index 0000000000000..6b83b11048fcf
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+
+/**
+ * The channel responsible for TSS
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class SpeakChannel extends MycroftChannel {
+
+ public SpeakChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.SPEAK_CHANNEL);
+ }
+
+ @Override
+ public List getMessageToListenTo() {
+ return Arrays.asList(MessageType.speak);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ if (message.type == MessageType.speak) {
+ MessageSpeak messageSpeak = (MessageSpeak) message;
+ updateMyState(new StringType(messageSpeak.data.utterance));
+ }
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof StringType) {
+ if (handler.sendMessage(new MessageSpeak(command.toFullString()))) {
+ updateMyState(new StringType(command.toFullString()));
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java
new file mode 100644
index 0000000000000..a019ca7b5a817
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopUtterance;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.Command;
+
+/**
+ * This channel handle the full utterance send or received by Mycroft, before any intent recognition
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class UtteranceChannel extends MycroftChannel {
+
+ public UtteranceChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.UTTERANCE_CHANNEL);
+ }
+
+ @Override
+ protected List getMessageToListenTo() {
+ return Arrays.asList(MessageType.recognizer_loop__utterance);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ if (message.type == MessageType.recognizer_loop__utterance) {
+ List utterances = ((MessageRecognizerLoopUtterance) message).data.utterances;
+ if (!utterances.isEmpty()) {
+ updateMyState(new StringType(utterances.get(0)));
+ }
+ }
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof StringType) {
+ if (handler.sendMessage(new MessageRecognizerLoopUtterance(command.toFullString()))) {
+ updateMyState(new StringType(command.toFullString()));
+ }
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java
new file mode 100644
index 0000000000000..dccd359db7608
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java
@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.channels;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.mycroft.internal.MycroftBindingConstants;
+import org.openhab.binding.mycroft.internal.MycroftHandler;
+import org.openhab.binding.mycroft.internal.api.MessageType;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDecrease;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGetResponse;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeIncrease;
+import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet;
+import org.openhab.core.library.types.IncreaseDecreaseType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+
+/**
+ * The channel responsible for handling the volume of the Mycroft speaker
+ * QUITE FUNCTIONAL but with workaround
+ * (see https://community.mycroft.ai/t/openhab-plugin-development-audio-volume-message-types-missing/10576
+ * and https://github.com/MycroftAI/skill-volume/issues/53)
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@NonNullByDefault
+public class VolumeChannel extends MycroftChannel {
+
+ /**
+ * As the MessageVolumeSet is, contrary to the documentation, not listened to by Mycroft,
+ * we use a workaround and send a message simulating an intent detection
+ */
+ public static final String VOLUME_SETTER_MESSAGE = "{\"type\": \"mycroft-volume.mycroftai:SetVolume\", \"data\": {\"intent_type\": \"mycroft-volume.mycroftai:SetVolume\", \"mycroft_volume_mycroftaiVolume\": \"volume\", \"mycroft_volume_mycroftaiLevel\": \"$$VOLUME\", \"mycroft_volume_mycroftaiTo\": \"to\", \"target\": null, \"confidence\": 0.6000000000000001, \"__tags__\": [{\"match\": \"volume\", \"key\": \"volume\", \"start_token\": 1, \"entities\": [{\"key\": \"volume\", \"match\": \"volume\", \"data\": [[\"volume\", \"mycroft_volume_mycroftaiVolume\"]], \"confidence\": 1.0}], \"end_token\": 1, \"from_context\": false}, {\"match\": \"$$VOLUME\", \"key\": \"$$VOLUME\", \"start_token\": 3, \"entities\": [{\"key\": \"$$VOLUME\", \"match\": \"$$VOLUME\", \"data\": [[\"$$VOLUME\", \"mycroft_volume_mycroftaiLevel\"]], \"confidence\": 1.0}], \"end_token\": 3, \"from_context\": false}, {\"match\": \"to\", \"key\": \"to\", \"start_token\": 2, \"entities\": [{\"key\": \"to\", \"match\": \"to\", \"data\": [[\"to\", \"mycroft_volume_mycroftaiTo\"]], \"confidence\": 1.0}], \"end_token\": 2, \"from_context\": false}], \"utterance\": \"set volume to $$VOLUME\", \"utterances\": [\"set volume to X\"]}, \"context\": {\"client_name\": \"mycroft_cli\", \"source\": [\"skills\"], \"destination\": \"debug_cli\"}}";
+
+ private PercentType lastVolume = new PercentType(50);
+ private PercentType lastNonZeroVolume = new PercentType(50);
+
+ public VolumeChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.VOLUME_CHANNEL);
+ }
+
+ @Override
+ public List getMessageToListenTo() {
+ // we don't listen to mute/unmute message because duck/unduck seems sufficient
+ // and we don't want to change state twice for the same event
+ // but it should be tested on mark I, as volume is handled differently
+ return Arrays.asList(MessageType.mycroft_volume_get_response, MessageType.mycroft_volume_set,
+ MessageType.mycroft_volume_increase, MessageType.mycroft_volume_decrease,
+ MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+
+ if (message.type == MessageType.mycroft_volume_get_response) {
+ float volumeGet = ((MessageVolumeGetResponse) message).data.percent;
+ updateAndSaveMyState(normalizeVolume(volumeGet));
+ } else if (message.type == MessageType.mycroft_volume_set) {
+ float volumeSet = ((MessageVolumeSet) message).data.percent;
+ updateAndSaveMyState(normalizeVolume(volumeSet));
+ } else if (message.type == MessageType.mycroft_volume_duck) {
+ updateAndSaveMyState(new PercentType(0));
+ } else if (message.type == MessageType.mycroft_volume_unduck) {
+ updateAndSaveMyState(lastNonZeroVolume);
+ } else if (message.type == MessageType.mycroft_volume_increase) {
+ updateAndSaveMyState(normalizeVolume(lastVolume.intValue() + 10));
+ } else if (message.type == MessageType.mycroft_volume_decrease) {
+ updateAndSaveMyState(normalizeVolume(lastVolume.intValue() - 10));
+ }
+ }
+
+ protected final void updateAndSaveMyState(State state) {
+ if (state instanceof PercentType) {
+ this.lastVolume = ((PercentType) state);
+ if (((PercentType) state).intValue() > 0) {
+ this.lastNonZeroVolume = ((PercentType) state);
+ }
+ }
+ super.updateMyState(state);
+ }
+
+ /**
+ * Protection method for volume with
+ * potentially wrong value.
+ *
+ * @param volume The requested volume, on a scale from 0 to 100.
+ * Could be out of bond, then it will be corrected.
+ * @return A safe volume in PercentType between 0 and 100
+ */
+ private PercentType normalizeVolume(int volume) {
+ if (volume >= 100) {
+ return PercentType.HUNDRED;
+ } else if (volume <= 0) {
+ return PercentType.ZERO;
+ } else {
+ return new PercentType(volume);
+ }
+ }
+
+ /**
+ * Protection method for volume with
+ * potentially wrong value.
+ *
+ * @param volume The requested volume, on a scale from 0 to 1.
+ * @return A safe volume in PercentType between 0 and 100
+ */
+ private PercentType normalizeVolume(float volume) {
+ if (volume >= 1) {
+ return PercentType.HUNDRED;
+ } else if (volume <= 0) {
+ return PercentType.ZERO;
+ } else {
+ return new PercentType(Math.round(volume * 100));
+ }
+ }
+
+ public float toMycroftVolume(PercentType percentType) {
+ return Float.valueOf(percentType.intValue());
+ }
+
+ public PercentType computeNewVolume(int valueAdded) {
+ return new PercentType(lastVolume.intValue() + valueAdded);
+ }
+
+ private boolean sendSetMessage(float volume) {
+ String messageToSend = VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME", Float.valueOf(volume).toString());
+ return handler.sendMessage(messageToSend);
+ }
+
+ @Override
+ public void handleCommand(Command command) {
+ if (command instanceof OnOffType) {
+ if (command == OnOffType.ON) {
+ if (sendSetMessage(toMycroftVolume(lastNonZeroVolume))) {
+ updateAndSaveMyState(lastNonZeroVolume);
+ }
+ }
+ if (command == OnOffType.OFF) {
+ if (sendSetMessage(0)) {
+ updateAndSaveMyState(PercentType.ZERO);
+ }
+ }
+ } else if (command instanceof IncreaseDecreaseType) {
+ if (command == IncreaseDecreaseType.INCREASE) {
+ if (handler.sendMessage(new MessageVolumeIncrease())) {
+ updateAndSaveMyState(computeNewVolume(10));
+ }
+ }
+ if (command == IncreaseDecreaseType.DECREASE) {
+ handler.sendMessage(new MessageVolumeDecrease());
+ updateAndSaveMyState(computeNewVolume(-10));
+ }
+ } else if (command instanceof PercentType) {
+ sendSetMessage(toMycroftVolume((PercentType) command));
+ updateAndSaveMyState((PercentType) command);
+ } else if (command instanceof RefreshType) {
+ handler.sendMessage(new MessageVolumeGet());
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..03d6cb09a67c5
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,15 @@
+
+
+
+ Mycroft Binding
+
+ Connects to a Mycroft instance in order to receive information from, and send commands to it. Typical
+ usage includes
+ triggering Mycroft to listen (as if a wake word was detected), sending text for Mycroft to speak,
+ reacting on some
+ specific intent, command skills by faking a spoken utterance, etc.
+
+
+
diff --git a/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/i18n/mycroft_fr.properties b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/i18n/mycroft_fr.properties
new file mode 100644
index 0000000000000..8f05dbb08bdee
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/i18n/mycroft_fr.properties
@@ -0,0 +1,34 @@
+# binding
+
+binding.mycroft.name = Extension Mycroft
+binding.mycroft.description = Cette extension se connecte à une enceinte Mycroft pour recevoir des informations et envoyer des commandes. Parmi les usages typiques : déclencher l'écoute de Mycroft (comme si le mot de réveil avait été détecté), envoyer un texte pour qu'il soit énoncé, réagir à un Intent, commander à des Skills comme si une phrase avait été prononcée, etc.
+
+# thing types
+
+thing-type.mycroft.mycroft.label = Mycroft
+thing-type.mycroft.mycroft.description = Une instance de Mycroft (Mark I/II, Picroft).
+
+# thing types config
+
+thing-type.config.mycroft.mycroft.host.label = Nom d'hôte
+thing-type.config.mycroft.mycroft.host.description = Nom d'hôte de l'instance.
+thing-type.config.mycroft.mycroft.port.label = Port
+thing-type.config.mycroft.mycroft.port.description = Port du bus de message.
+thing-type.config.mycroft.mycroft.volume_restoration_level.label = Niveau du volume de restauration
+thing-type.config.mycroft.mycroft.volume_restoration_level.description = Quand le volume est restauré, force Mycroft a le régler à cette valeur.
+
+# channel types
+
+channel-type.mycroft.full-message-channel.label = Message complet
+channel-type.mycroft.full-message-channel.description = Le dernier message qui a été vu sur le bus de message.
+channel-type.mycroft.listen-channel.label = État de l'écoute
+channel-type.mycroft.listen-channel.description = Allumé quand Mycroft écoute activement. Peut du coup simuler le mot de réveil.
+channel-type.mycroft.speak-channel.label = Synthèse vocale
+channel-type.mycroft.speak-channel.description = Phrase énoncée par Mycroft.
+channel-type.mycroft.utterance-channel.label = Commande vocale
+channel-type.mycroft.utterance-channel.description = Commande vocale reçue par Mycroft.
+
+# channel types config
+
+channel-type.config.mycroft.full-message-channel.messageTypes.label = Filtre du canal Message complet
+channel-type.config.mycroft.full-message-channel.messageTypes.description = Le canal Message complet sera mis à jour uniquement pour ces types de messages (liste séparée par une virgule)
diff --git a/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..0f9106a6310dd
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+ A Mycroft instance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This is the host to connect to (ip or hostname)
+ network-address
+
+
+
+ This is the port to connect to.
+ 8181
+
+
+ true
+
+ When unmuted, force Mycroft to restore volume to this value
+
+
+
+
+
+
+
+ Switch
+
+ Switch to ON when Mycroft is listening. Can simulate a wake work detection to trigger the STT.
+
+
+
+ String
+
+ The last sentence Mycroft spoke.
+
+
+
+ String
+
+ The last utterance Mycroft received.
+
+
+
+ String
+
+ The last full message seen on the Mycroft Bus.
+
+
+
+ The full message channel will be updated on these message types only (comma separated value)
+ message.type.1,message.type.2
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java b/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java
new file mode 100644
index 0000000000000..23d3ec052a8fe
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.mycroft.internal.api;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.InetSocketAddress;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.websocket.api.Session;
+import org.eclipse.jetty.websocket.client.WebSocketClient;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
+import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak;
+
+/**
+ * This class provides tests for mycroft binding
+ *
+ * @author Gwendal Roulleau - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+@NonNullByDefault
+public class MycroftConnectionTest {
+
+ private @Mock @NonNullByDefault({}) MycroftConnectionListener mycroftConnectionListener;
+ private @Mock @NonNullByDefault({}) Session sessionMock;
+
+ @Test
+ public void testConnectionOK() throws IOException {
+
+ MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient());
+ Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234));
+ mycroftConnection.onConnect(sessionMock);
+
+ Mockito.verify(mycroftConnectionListener, Mockito.times(1)).connectionEstablished();
+ }
+
+ @Test
+ public void testAnyListener() throws UnsupportedEncodingException, IOException {
+ MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient());
+
+ Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234));
+ mycroftConnection.onConnect(sessionMock);
+
+ @SuppressWarnings("unchecked")
+ MycroftMessageListener mockListener = Mockito.mock(MycroftMessageListener.class);
+ ArgumentCaptor argCaptorMessage = ArgumentCaptor.forClass(BaseMessage.class);
+
+ // given we register any listener
+ mycroftConnection.registerListener(MessageType.any, mockListener);
+
+ // when we send speak message
+ @SuppressWarnings("null")
+ String speakMessageJson = new String(
+ MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8");
+ mycroftConnection.onMessage(sessionMock, speakMessageJson);
+
+ // then message is correctly received by listener
+ Mockito.verify(mockListener, Mockito.times(1)).baseMessageReceived(ArgumentMatchers.any());
+ Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture());
+
+ assertEquals(argCaptorMessage.getValue().message, speakMessageJson);
+ }
+
+ @Test
+ public void testSpeakListener() throws IOException {
+
+ MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient());
+
+ Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234));
+ mycroftConnection.onConnect(sessionMock);
+
+ @SuppressWarnings("unchecked")
+ MycroftMessageListener mockListener = Mockito.mock(MycroftMessageListener.class);
+ ArgumentCaptor argCaptorMessage = ArgumentCaptor.forClass(MessageSpeak.class);
+
+ // given we register speak listener
+ mycroftConnection.registerListener(MessageType.speak, mockListener);
+
+ // when we send speak message
+ @SuppressWarnings("null")
+ String speakMessageJson = new String(
+ MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8");
+ mycroftConnection.onMessage(sessionMock, speakMessageJson);
+
+ // then message is correctly received by listener
+ Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture());
+
+ assertEquals(argCaptorMessage.getValue().data.utterance, "coucou");
+ }
+}
diff --git a/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json b/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json
new file mode 100644
index 0000000000000..dcb8697be4d86
--- /dev/null
+++ b/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json
@@ -0,0 +1 @@
+{"type": "speak", "data": {"utterance": "coucou", "expect_response": false, "meta": {"skill": "SpeakSkill"}, "is_error": false}, "context": {"client_name": "mycroft_cli", "source": ["skills"], "destination": "debug_cli"}}
\ No newline at end of file
diff --git a/bundles/org.openhab.binding.playstation/src/main/resources/OH-INF/i18n/playstation.properties b/bundles/org.openhab.binding.playstation/src/main/resources/OH-INF/i18n/playstation.properties
old mode 100644
new mode 100755
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 62cc68f539505..27c9d92cc68bb 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -228,6 +228,7 @@
org.openhab.binding.mqtt.genericorg.openhab.binding.mqtt.homeassistantorg.openhab.binding.mqtt.homie
+ org.openhab.binding.mycroftorg.openhab.binding.myqorg.openhab.binding.mystromorg.openhab.binding.nanoleaf