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