This binding will connect to Mycroft A.I. in order to control it or react to event by listening on the message bus.
Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
/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
<artifactId>org.openhab.binding.mqtt.homie</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.binding.mycroft</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.myq</artifactId>
--- /dev/null
+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
--- /dev/null
+# 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
+```
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.openhab.addons.bundles</groupId>
+ <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+ <version>3.3.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>org.openhab.binding.mycroft</artifactId>
+
+ <name>openHAB Add-ons :: Bundles :: Mycroft Binding</name>
+
+</project>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.mycroft-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+ <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+ <feature name="openhab-binding-mycroft" description="mycroft Binding" version="${project.version}">
+ <feature>openhab-runtime-base</feature>
+ <feature>openhab-transport-http</feature>
+ <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mycroft/${project.version}</bundle>
+ </feature>
+</features>
--- /dev/null
+/**
+ * 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";
+}
--- /dev/null
+/**
+ * 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;
+}
--- /dev/null
+/**
+ * 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<ChannelUID, MycroftChannel<?>> 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<ChannelUID, MycroftChannel<?>> 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 <T extends State> void updateMyChannel(MycroftChannel<T> 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;
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<ThingTypeUID> 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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<MessageType>, JsonSerializer<MessageType> {
+ @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());
+ }
+}
--- /dev/null
+/**
+ * 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<MessageType, Set<MycroftMessageListener<? extends BaseMessage>>> 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<Session> 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<MycroftMessageListener<? extends BaseMessage>> messageTypeListeners = listeners.get(messageType);
+ if (messageTypeListeners == null) {
+ messageTypeListeners = new HashSet<MycroftMessageListener<? extends BaseMessage>>();
+ 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
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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<T extends BaseMessage> {
+ /**
+ * 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");
+ }
+ }
+}
--- /dev/null
+/**
+ * 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 = "";
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 = "";
+ }
+}
--- /dev/null
+/**
+ * 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 = "";
+ }
+}
--- /dev/null
+/**
+ * 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<String> utterances = new ArrayList<>();
+ }
+
+ public static class Context {
+ public String client_name = "";
+ public String source = "";
+ public List<String> destination = new ArrayList<>();
+ }
+}
--- /dev/null
+/**
+ * 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<String> source = new ArrayList<>();
+ public String destination = "";
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 {
+ }
+}
--- /dev/null
+/**
+ * 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 {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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 {
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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<State> {
+
+ public AudioPlayerChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.PLAYER_CHANNEL);
+ }
+
+ @Override
+ protected List<MessageType> 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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<StringType> {
+
+ private List<String> 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<MessageType> 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()));
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<OnOffType> {
+
+ public ListenChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.LISTEN_CHANNEL);
+ }
+
+ @Override
+ public List<MessageType> 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());
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<OnOffType> {
+
+ private int volumeRestorationLevel;
+
+ public MuteChannel(MycroftHandler handler, int volumeRestorationLevel) {
+ super(handler, MycroftBindingConstants.VOLUME_MUTE_CHANNEL);
+ this.volumeRestorationLevel = volumeRestorationLevel;
+ }
+
+ @Override
+ public List<MessageType> 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));
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<T extends State>
+ implements ChannelCommandHandler, MycroftMessageListener<BaseMessage> {
+
+ 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<MessageType> getMessageToListenTo() {
+ return new ArrayList<>();
+ }
+
+ public final void unregisterListeners() {
+ for (MessageType messageType : getMessageToListenTo()) {
+ handler.unregisterMessageListener(messageType, this);
+ }
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ }
+}
--- /dev/null
+/**
+ * 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<StringType> {
+
+ public SpeakChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.SPEAK_CHANNEL);
+ }
+
+ @Override
+ public List<MessageType> 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()));
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<StringType> {
+
+ public UtteranceChannel(MycroftHandler handler) {
+ super(handler, MycroftBindingConstants.UTTERANCE_CHANNEL);
+ }
+
+ @Override
+ protected List<MessageType> getMessageToListenTo() {
+ return Arrays.asList(MessageType.recognizer_loop__utterance);
+ }
+
+ @Override
+ public void messageReceived(BaseMessage message) {
+ if (message.type == MessageType.recognizer_loop__utterance) {
+ List<String> 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()));
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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<State> {
+
+ /**
+ * 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<MessageType> 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());
+ }
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="mycroft" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
+
+ <name>Mycroft Binding</name>
+ <description>
+ 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.
+ </description>
+
+</binding:binding>
--- /dev/null
+# 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)
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="mycroft"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
+ xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
+
+ <thing-type id="mycroft">
+ <label>Mycroft</label>
+ <description>A Mycroft instance</description>
+
+ <channels>
+ <channel id="listen" typeId="listen-channel"/>
+ <channel id="speak" typeId="speak-channel"/>
+ <channel id="utterance" typeId="utterance-channel"/>
+ <channel id="player" typeId="system.media-control"/>
+ <channel id="volume" typeId="system.volume"/>
+ <channel id="volume_mute" typeId="system.mute"/>
+ <channel id="full_message" typeId="full-message-channel"/>
+ </channels>
+
+ <config-description>
+ <parameter name="host" type="text" required="true">
+ <label>Hostname</label>
+ <description>This is the host to connect to (ip or hostname)</description>
+ <context>network-address</context>
+ </parameter>
+ <parameter name="port" type="integer" required="false" min="1" max="65535">
+ <label>Port</label>
+ <description>This is the port to connect to.</description>
+ <default>8181</default>
+ </parameter>
+ <parameter name="volume_restoration_level" type="integer" required="false" min="1" max="100">
+ <advanced>true</advanced>
+ <label>Volume Restoration Level</label>
+ <description>When unmuted, force Mycroft to restore volume to this value</description>
+ </parameter>
+
+ </config-description>
+
+ </thing-type>
+
+ <channel-type id="listen-channel">
+ <item-type>Switch</item-type>
+ <label>Listen State</label>
+ <description>Switch to ON when Mycroft is listening. Can simulate a wake work detection to trigger the STT.</description>
+ </channel-type>
+
+ <channel-type id="speak-channel">
+ <item-type>String</item-type>
+ <label>TTS</label>
+ <description>The last sentence Mycroft spoke.</description>
+ </channel-type>
+
+ <channel-type id="utterance-channel">
+ <item-type>String</item-type>
+ <label>Utterance</label>
+ <description>The last utterance Mycroft received.</description>
+ </channel-type>
+
+ <channel-type id="full-message-channel" advanced="true">
+ <item-type>String</item-type>
+ <label>Full Bus Message</label>
+ <description>The last full message seen on the Mycroft Bus.</description>
+ <config-description>
+ <parameter name="messageTypes" type="text" required="false">
+ <label>Full Message Channel Filter</label>
+ <description>The full message channel will be updated on these message types only (comma separated value)</description>
+ <default>message.type.1,message.type.2</default>
+ </parameter>
+ </config-description>
+ </channel-type>
+
+</thing:thing-descriptions>
--- /dev/null
+/**
+ * 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<MessageSpeak> mockListener = Mockito.mock(MycroftMessageListener.class);
+ ArgumentCaptor<BaseMessage> 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<MessageSpeak> mockListener = Mockito.mock(MycroftMessageListener.class);
+ ArgumentCaptor<MessageSpeak> 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");
+ }
+}
--- /dev/null
+{"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
<module>org.openhab.binding.mqtt.generic</module>
<module>org.openhab.binding.mqtt.homeassistant</module>
<module>org.openhab.binding.mqtt.homie</module>
+ <module>org.openhab.binding.mycroft</module>
<module>org.openhab.binding.myq</module>
<module>org.openhab.binding.mystrom</module>
<module>org.openhab.binding.nanoleaf</module>