[mycroft] Initial contribution (#11040)

This binding will connect to Mycroft A.I. in order to control it or react to event by listening on the message bus.

Signed-off-by: Gwendal Roulleau <gwendal.roulleau@gmail.com>
This commit is contained in:
dalgwen 2022-01-10 16:51:07 +01:00 committed by GitHub
parent ab1ce7f6db
commit 40b5932817
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 2783 additions and 0 deletions

View File

@ -198,6 +198,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

View File

@ -976,6 +976,11 @@
<artifactId>org.openhab.binding.mqtt.homie</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.mycroft</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.myq</artifactId>

View File

@ -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

View File

@ -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
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.mycroft</artifactId>
<name>openHAB Add-ons :: Bundles :: Mycroft Binding</name>
</project>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.mycroft-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-mycroft" description="mycroft Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<feature>openhab-transport-http</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.mycroft/${project.version}</bundle>
</feature>
</features>

View File

@ -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";
}

View File

@ -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;
}

View File

@ -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<ChannelUID, MycroftChannel<?>> mycroftChannels = new HashMap<>();
/** The reconnect frequency in case of error */
private static final int POLL_FREQUENCY_SEC = 30;
private int sometimesSendVolumeRequest = 0;
public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) {
super(thing);
this.webSocketFactory = webSocketFactory;
}
/**
* Stops the API request or websocket reconnect timer
*/
private void stopTimer() {
ScheduledFuture<?> future = scheduledFuture;
if (future != null) {
future.cancel(false);
scheduledFuture = null;
}
}
/**
* Starts the websocket connection.
* It sometimes also sends a get volume request to check the connection and refresh the volume.
*/
private void checkOrstartWebsocket() {
if (thingDisposing) {
return;
}
if (connection.isConnected()) {
// sometimes test the connection by sending a real message
// AND refreshing volume in the same step
if (sometimesSendVolumeRequest >= 3) { // arbitrary one on three times
sometimesSendVolumeRequest = 0;
sendMessage(new MessageVolumeGet());
} else {
sometimesSendVolumeRequest++;
}
} else {
connection.start(config.host, config.port);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
ChannelCommandHandler channelCommand = mycroftChannels.get(channelUID);
if (channelCommand == null) {
logger.error("Command {} for channel {} cannot be handled", command.toString(), channelUID.toString());
} else {
channelCommand.handleCommand(command);
}
}
@Override
public void initialize() {
thingDisposing = false;
updateStatus(ThingStatus.UNKNOWN);
logger.debug("Start initializing Mycroft {}", thing.getUID());
String websocketID = thing.getUID().getAsString().replace(':', '-');
if (websocketID.length() < 4) {
websocketID = "mycroft-" + websocketID;
}
if (websocketID.length() > 20) {
websocketID = websocketID.substring(websocketID.length() - 20);
}
this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID));
config = getConfigAs(MycroftConfiguration.class);
if (config.host.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING, "No host defined");
return;
} else if (config.port < 0 || config.port > 0xFFFF) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Port should be between 0 and 65536");
return;
}
scheduledFuture = scheduler.scheduleWithFixedDelay(this::checkOrstartWebsocket, 0, POLL_FREQUENCY_SEC,
TimeUnit.SECONDS);
registerChannel(new ListenChannel(this));
registerChannel(new VolumeChannel(this));
registerChannel(new MuteChannel(this, config.volume_restoration_level));
registerChannel(new SpeakChannel(this));
registerChannel(new AudioPlayerChannel(this));
registerChannel(new UtteranceChannel(this));
final Channel fullMessageChannel = getThing().getChannel(MycroftBindingConstants.FULL_MESSAGE_CHANNEL);
@SuppressWarnings("null") // cannot be null
String messageTypesProperty = (String) fullMessageChannel.getConfiguration()
.get(MycroftBindingConstants.FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY);
registerChannel(new FullMessageChannel(this, messageTypesProperty));
checkLinkedChannelsAndRegisterMessageListeners();
}
private void checkLinkedChannelsAndRegisterMessageListeners() {
for (Entry<ChannelUID, MycroftChannel<?>> channelEntry : mycroftChannels.entrySet()) {
ChannelUID uid = channelEntry.getKey();
MycroftChannel<?> channel = channelEntry.getValue();
if (isLinked(uid)) {
channel.registerListeners();
} else {
channel.unregisterListeners();
}
}
}
@Override
public void channelLinked(ChannelUID channelUID) {
checkLinkedChannelsAndRegisterMessageListeners();
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
checkLinkedChannelsAndRegisterMessageListeners();
}
private void registerChannel(MycroftChannel<?> channel) {
mycroftChannels.put(channel.getChannelUID(), channel);
}
public void registerMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
this.connection.registerListener(messageType, listener);
}
public void unregisterMessageListener(MessageType messageType, MycroftMessageListener<?> listener) {
this.connection.unregisterListener(messageType, listener);
}
@Override
public void connectionEstablished() {
logger.debug("Mycroft thing {} is online", thing.getUID());
updateStatus(ThingStatus.ONLINE);
}
@Override
public void connectionLost(String reason) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason);
}
@Override
public void dispose() {
thingDisposing = true;
stopTimer();
connection.close();
}
public <T extends State> void updateMyChannel(MycroftChannel<T> mycroftChannel, T state) {
updateState(mycroftChannel.getChannelUID(), state);
}
public boolean sendMessage(BaseMessage message) {
try {
connection.sendMessage(message);
return true;
} catch (IOException e) {
logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
return false;
}
}
public boolean sendMessage(String message) {
try {
connection.sendMessage(message);
return true;
} catch (IOException e) {
logger.debug("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage());
return false;
}
}
}

View File

@ -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<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(MycroftBindingConstants.MYCROFT);
private final WebSocketFactory webSocketFactory;
@Activate
public MycroftHandlerFactory(final @Reference WebSocketFactory webSocketFactory) {
this.webSocketFactory = webSocketFactory;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (MycroftBindingConstants.MYCROFT.equals(thingTypeUID)) {
return new MycroftHandler(thing, webSocketFactory);
}
return null;
}
}

View File

@ -0,0 +1,114 @@
/**
* Copyright (c) 2010-2022 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.mycroft.internal.api;
import java.util.stream.Stream;
import javax.validation.constraints.NotNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.mycroft.internal.api.dto.BaseMessage;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioNext;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPause;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPlay;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPrev;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioResume;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioStop;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfo;
import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfoReply;
import org.openhab.binding.mycroft.internal.api.dto.MessageMicListen;
import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordBegin;
import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordEnd;
import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopUtterance;
import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDecrease;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDuck;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGetResponse;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeIncrease;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeMute;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnduck;
import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnmute;
/**
* All message type of interest, issued by Mycroft, are referenced here
*
* @author Gwendal Roulleau - Initial contribution
*/
@NonNullByDefault
public enum MessageType {
any("special-anymessages", BaseMessage.class),
speak("speak", MessageSpeak.class),
recognizer_loop__record_begin("recognizer_loop:record_begin", MessageRecognizerLoopRecordBegin.class),
recognizer_loop__record_end("recognizer_loop:record_end", MessageRecognizerLoopRecordEnd.class),
recognizer_loop__utterance("recognizer_loop:utterance", MessageRecognizerLoopUtterance.class),
mycroft_mic_listen("mycroft.mic.listen", MessageMicListen.class),
mycroft_audio_service_pause("mycroft.audio.service.pause", MessageAudioPause.class),
mycroft_audio_service_resume("mycroft.audio.service.resume", MessageAudioResume.class),
mycroft_audio_service_stop("mycroft.audio.service.stop", MessageAudioStop.class),
mycroft_audio_service_play("mycroft.audio.service.play", MessageAudioPlay.class),
mycroft_audio_service_next("mycroft.audio.service.next", MessageAudioNext.class),
mycroft_audio_service_prev("mycroft.audio.service.prev", MessageAudioPrev.class),
mycroft_audio_service_track_info("mycroft.audio.service.track_info", MessageAudioTrackInfo.class),
mycroft_audio_service_track_info_reply("mycroft.audio.service.track_info_reply", MessageAudioTrackInfoReply.class),
mycroft_volume_set("mycroft.volume.set", MessageVolumeSet.class),
mycroft_volume_increase("mycroft.volume.increase", MessageVolumeIncrease.class),
mycroft_volume_decrease("mycroft.volume.decrease", MessageVolumeDecrease.class),
mycroft_volume_get("mycroft.volume.get", MessageVolumeGet.class),
mycroft_volume_get_response("mycroft.volume.get.response", MessageVolumeGetResponse.class),
mycroft_volume_mute("mycroft.volume.mute", MessageVolumeMute.class),
mycroft_volume_unmute("mycroft.volume.unmute", MessageVolumeUnmute.class),
mycroft_volume_duck("mycroft.volume.duck", MessageVolumeDuck.class),
mycroft_volume_unduck("mycroft.volume.unduck", MessageVolumeUnduck.class),
mycroft_reminder_mycroftai__reminder("mycroft-reminder.mycroftai:reminder", BaseMessage.class),
mycroft_date_time_mycroftai__timeskillupdate_display("mycroft-date-time.mycroftai:TimeSkillupdate_display",
BaseMessage.class),
mycroft_configuration_mycroftai__configurationskillupdate_remote(
"mycroft-configuration.mycroftai:ConfigurationSkillupdate_remote", BaseMessage.class);
private @NotNull Class<? extends BaseMessage> messageTypeClass;
private @NotNull String messageTypeName;
MessageType(String messageTypeName, Class<? extends BaseMessage> messageType) {
this.messageTypeClass = messageType;
this.messageTypeName = messageTypeName;
}
/**
* Get the expected message type for this message
*
* @return The message type class associated with this type
*/
public @NotNull Class<? extends BaseMessage> getMessageTypeClass() {
return messageTypeClass;
}
@NotNull
public static MessageType fromString(String asString) {
return Stream.of(values()).filter(messageType -> messageType.messageTypeName.equals(asString)).findFirst()
.orElse(any);
}
public String getMessageTypeName() {
return messageTypeName;
}
protected void setMessageTypeName(String messageTypeName) {
this.messageTypeName = messageTypeName;
}
}

View File

@ -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<MessageType>, JsonSerializer<MessageType> {
@Override
public @Nullable MessageType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
MessageType messageType = MessageType.fromString(json.getAsString());
// for message of type non recognized :
messageType.setMessageTypeName(json.getAsString());
return messageType;
}
@Override
public JsonElement serialize(MessageType src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getMessageTypeName());
}
}

View File

@ -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<MessageType, Set<MycroftMessageListener<? extends BaseMessage>>> listeners = new ConcurrentHashMap<>();
private ConnectionState connectionState = ConnectionState.DISCONNECTED;
private @Nullable Session session;
private static final int TIMEOUT_MILLISECONDS = 3000;
public MycroftConnection(MycroftConnectionListener listener, WebSocketClient client) {
this.connectionListener = listener;
this.client = client;
this.client.setConnectTimeout(TIMEOUT_MILLISECONDS);
this.client.setMaxIdleTimeout(0);
this.socketName = "Websocket-Mycroft$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet();
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(MessageType.class, new MessageTypeConverter());
gson = gsonBuilder.create();
}
public MycroftConnection(MycroftConnectionListener listener) {
this(listener, new WebSocketClient());
}
public void start(String ip, int port) {
if (connectionState == ConnectionState.CONNECTED) {
return;
} else if (connectionState == ConnectionState.CONNECTING) {
logger.debug("{} already connecting", socketName);
return;
} else if (connectionState == ConnectionState.DISCONNECTING) {
logger.warn("{} trying to re-connect while still disconnecting", socketName);
}
Future<Session> futureConnect = null;
try {
URI destUri = URI.create("ws://" + ip + ":" + port + "/core");
client.start();
logger.debug("Trying to connect {} to {}", socketName, destUri);
futureConnect = client.connect(this, destUri);
futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS);
} catch (Exception e) {
if (futureConnect != null) {
futureConnect.cancel(true);
}
connectionListener
.connectionLost("Error while connecting: " + (e.getMessage() != null ? e.getMessage() : "unknown"));
}
}
public void close() {
try {
connectionState = ConnectionState.DISCONNECTING;
client.stop();
} catch (Exception e) {
logger.debug("{} encountered an error while closing connection", socketName, e);
}
client.destroy();
}
/**
* The listener registered in this method will be called when a corresponding message will be detected
* on the Mycroft bus.
*
* @param messageType The message type to listen to.
* @param listener The listener will receive a callback when the requested message type will be detected on the bus.
*/
public void registerListener(MessageType messageType, MycroftMessageListener<? extends BaseMessage> listener) {
Set<MycroftMessageListener<? extends BaseMessage>> messageTypeListeners = listeners.get(messageType);
if (messageTypeListeners == null) {
messageTypeListeners = new HashSet<MycroftMessageListener<? extends BaseMessage>>();
listeners.put(messageType, messageTypeListeners);
}
messageTypeListeners.add(listener);
}
public void unregisterListener(MessageType messageType, MycroftMessageListener<?> listener) {
Optional.ofNullable(listeners.get(messageType))
.ifPresent((messageTypeListeners) -> messageTypeListeners.remove(listener));
}
public void sendMessage(BaseMessage message) throws IOException {
sendMessage(gson.toJson(message));
}
public void sendMessage(String message) throws IOException {
final Session storedSession = this.session;
try {
if (storedSession != null) {
storedSession.getRemote().sendString(message);
} else {
throw new IOException("Session is not initialized");
}
} catch (IOException e) {
if (storedSession != null && storedSession.isOpen()) {
storedSession.close(-1, "Sending message error");
}
throw e;
}
}
@OnWebSocketConnect
public void onConnect(Session session) {
connectionState = ConnectionState.CONNECTED;
logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(),
session.hashCode());
connectionListener.connectionEstablished();
this.session = session;
}
@OnWebSocketMessage
public void onMessage(Session session, String message) {
if (!session.equals(this.session)) {
handleWrongSession(session, message);
return;
}
logger.trace("{} received raw data: {}", socketName, message);
try {
// get the base message information :
BaseMessage mycroftMessage = gson.fromJson(message, BaseMessage.class);
Objects.requireNonNull(mycroftMessage);
// now that we have the message type, we can use a second and more precise parsing:
if (mycroftMessage.type != MessageType.any) {
mycroftMessage = gson.fromJson(message, mycroftMessage.type.getMessageTypeClass());
Objects.requireNonNull(mycroftMessage);
}
// adding the raw message:
mycroftMessage.message = message;
final BaseMessage finalMessage = mycroftMessage;
Stream.concat(listeners.getOrDefault(MessageType.any, new HashSet<>()).stream(),
listeners.getOrDefault(mycroftMessage.type, new HashSet<>()).stream()).forEach(listener -> {
listener.baseMessageReceived(finalMessage);
});
} catch (RuntimeException e) {
// we need to catch all processing exceptions, otherwise they could affect the connection
logger.debug("{} encountered an error while processing the message {}: {}", socketName, message,
e.getMessage());
}
}
@OnWebSocketError
public void onError(@Nullable Session session, Throwable cause) {
if (session == null || !session.equals(this.session)) {
handleWrongSession(session, "Connection error: " + cause.getMessage());
return;
}
logger.debug("{} connection error, closing: {}", socketName, cause.getMessage());
Session storedSession = this.session;
if (storedSession != null && storedSession.isOpen()) {
storedSession.close(-1, "Processing error");
}
}
@OnWebSocketClose
public void onClose(Session session, int statusCode, String reason) {
if (!session.equals(this.session)) {
handleWrongSession(session, "Connection closed: " + statusCode + " / " + reason);
return;
}
logger.trace("{} closed connection: {} / {}", socketName, statusCode, reason);
connectionState = ConnectionState.DISCONNECTED;
this.session = null;
connectionListener.connectionLost(reason);
}
private void handleWrongSession(@Nullable Session session, String message) {
if (session == null) {
logger.debug("received and discarded message for null session : {}", message);
} else {
logger.debug("{} received and discarded message for other session {}: {}.", socketName, session.hashCode(),
message);
}
}
/**
* check connection state (successfully connected)
*
* @return true if connected, false if connecting, disconnecting or disconnected
*/
public boolean isConnected() {
return connectionState == ConnectionState.CONNECTED;
}
/**
* used internally to represent the connection state
*/
private enum ConnectionState {
CONNECTING,
CONNECTED,
DISCONNECTING,
DISCONNECTED
}
}

View File

@ -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);
}

View File

@ -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<T extends BaseMessage> {
/**
* A new message was received
*
* @param message The received message
*/
void messageReceived(T message);
@SuppressWarnings("unchecked")
default void baseMessageReceived(BaseMessage baseMessage) {
try {
messageReceived(((T) baseMessage));
} catch (ClassCastException cce) {
throw new ClassCastException("Incorrect use of message in Mycroft binding");
}
}
}

View File

@ -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 = "";
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 = "";
}
}

View File

@ -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 = "";
}
}

View File

@ -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<String> utterances = new ArrayList<>();
}
public static class Context {
public String client_name = "";
public String source = "";
public List<String> destination = new ArrayList<>();
}
}

View File

@ -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<String> source = new ArrayList<>();
public String destination = "";
}
}

View File

@ -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;
}
}

View File

@ -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 {
}
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {
}
}

View File

@ -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;
}
}

View File

@ -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<State> {
public AudioPlayerChannel(MycroftHandler handler) {
super(handler, MycroftBindingConstants.PLAYER_CHANNEL);
}
@Override
protected List<MessageType> getMessageToListenTo() {
return Arrays.asList(MessageType.mycroft_audio_service_prev, MessageType.mycroft_audio_service_next,
MessageType.mycroft_audio_service_pause, MessageType.mycroft_audio_service_resume,
MessageType.mycroft_audio_service_play, MessageType.mycroft_audio_service_stop,
MessageType.mycroft_audio_service_track_info, MessageType.mycroft_audio_service_track_info_reply);
}
@Override
public void messageReceived(BaseMessage message) {
switch (message.type) {
case mycroft_audio_service_pause:
case mycroft_audio_service_stop:
updateMyState(PlayPauseType.PAUSE);
break;
case mycroft_audio_service_play:
case mycroft_audio_service_resume:
updateMyState(PlayPauseType.PLAY);
break;
default:
break;
}
}
@Override
public void handleCommand(Command command) {
if (command instanceof PlayPauseType) {
if (((PlayPauseType) command) == PlayPauseType.PAUSE) {
if (handler.sendMessage(new MessageAudioPause())) {
updateMyState(PlayPauseType.PAUSE);
}
}
if (((PlayPauseType) command) == PlayPauseType.PLAY) {
handler.sendMessage(new MessageAudioPlay());
if (handler.sendMessage(new MessageAudioResume())) {
updateMyState(PlayPauseType.PLAY);
}
}
}
if (command instanceof NextPreviousType) {
if (((NextPreviousType) command) == NextPreviousType.NEXT) {
if (handler.sendMessage(new MessageAudioNext())) {
updateMyState(PlayPauseType.PLAY);
}
}
if (((NextPreviousType) command) == NextPreviousType.PREVIOUS) {
if (handler.sendMessage(new MessageAudioPrev())) {
updateMyState(PlayPauseType.PLAY);
}
}
}
}
}

View File

@ -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);
}

View File

@ -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<StringType> {
private List<String> messageTypesList = new ArrayList<>();
public FullMessageChannel(MycroftHandler handler, String messageTypesList) {
super(handler, MycroftBindingConstants.FULL_MESSAGE_CHANNEL);
for (String messageType : messageTypesList.split(",")) {
this.messageTypesList.add(messageType.trim());
}
}
@Override
public List<MessageType> getMessageToListenTo() {
return Arrays.asList(MessageType.any);
}
@Override
public void messageReceived(BaseMessage message) {
if (messageTypesList.contains(message.type.getMessageTypeName())) {
updateMyState(new StringType(message.message));
}
}
@Override
public void handleCommand(Command command) {
if (command instanceof StringType) {
if (handler.sendMessage(command.toFullString())) {
updateMyState(new StringType(command.toFullString()));
}
}
}
}

View File

@ -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<OnOffType> {
public ListenChannel(MycroftHandler handler) {
super(handler, MycroftBindingConstants.LISTEN_CHANNEL);
}
@Override
public List<MessageType> getMessageToListenTo() {
return Arrays.asList(MessageType.recognizer_loop__record_begin, MessageType.recognizer_loop__record_end);
}
@Override
public void messageReceived(BaseMessage message) {
if (message.type == MessageType.recognizer_loop__record_begin) {
updateMyState(OnOffType.ON);
} else if (message.type == MessageType.recognizer_loop__record_end) {
updateMyState(OnOffType.OFF);
}
}
@Override
public void handleCommand(Command command) {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
handler.sendMessage(new MessageMicListen());
}
}
}
}

View File

@ -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<OnOffType> {
private int volumeRestorationLevel;
public MuteChannel(MycroftHandler handler, int volumeRestorationLevel) {
super(handler, MycroftBindingConstants.VOLUME_MUTE_CHANNEL);
this.volumeRestorationLevel = volumeRestorationLevel;
}
@Override
public List<MessageType> getMessageToListenTo() {
// we don't listen to mute/unmute message because duck/unduck seems sufficient
// and we don't want to change state twice for the same event
// but it should be tested on mark I, as volume is handled differently
return Arrays.asList(MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck,
MessageType.mycroft_volume_set, MessageType.mycroft_volume_increase);
}
@Override
public void messageReceived(BaseMessage message) {
switch (message.type) {
case mycroft_volume_mute:
case mycroft_volume_duck:
updateMyState(OnOffType.ON);
break;
case mycroft_volume_unmute:
case mycroft_volume_unduck:
case mycroft_volume_increase:
updateMyState(OnOffType.OFF);
break;
case mycroft_volume_set:
if (((MessageVolumeSet) message).data.percent > 0) {
updateMyState(OnOffType.OFF);
}
break;
default:
}
}
private boolean sendVolumeSetMessage(float volume) {
String messageToSend = VolumeChannel.VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME",
Float.valueOf(volume).toString());
return handler.sendMessage(messageToSend);
}
@Override
public void handleCommand(Command command) {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
if (handler.sendMessage(new MessageVolumeMute())) {
updateMyState(OnOffType.ON);
}
} else if (command == OnOffType.OFF) {
if (handler.sendMessage(new MessageVolumeUnmute())) {
updateMyState(OnOffType.OFF);
// if configured, we can restore the volume to a fixed amount
// usefull as a workaround for the broken Mycroft volume behavior
if (volumeRestorationLevel > 0) {
// we must wait 100ms for Mycroft to handle the message and
// setting old volume before forcing to our value
try {
Thread.sleep(100);
} catch (InterruptedException e) {
}
sendVolumeSetMessage(Float.valueOf(volumeRestorationLevel));
}
}
}
}
}
}

View File

@ -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<T extends State>
implements ChannelCommandHandler, MycroftMessageListener<BaseMessage> {
private ChannelUID channelUID;
protected MycroftHandler handler;
public MycroftChannel(MycroftHandler handler, String channelUIDPart) {
this.handler = handler;
this.channelUID = new ChannelUID(handler.getThing().getUID(), channelUIDPart);
}
public final ChannelUID getChannelUID() {
return channelUID;
}
protected final void updateMyState(T state) {
handler.updateMyChannel(this, state);
}
public final void registerListeners() {
for (MessageType messageType : getMessageToListenTo()) {
handler.registerMessageListener(messageType, this);
}
}
protected List<MessageType> getMessageToListenTo() {
return new ArrayList<>();
}
public final void unregisterListeners() {
for (MessageType messageType : getMessageToListenTo()) {
handler.unregisterMessageListener(messageType, this);
}
}
@Override
public void messageReceived(BaseMessage message) {
}
}

View File

@ -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<StringType> {
public SpeakChannel(MycroftHandler handler) {
super(handler, MycroftBindingConstants.SPEAK_CHANNEL);
}
@Override
public List<MessageType> getMessageToListenTo() {
return Arrays.asList(MessageType.speak);
}
@Override
public void messageReceived(BaseMessage message) {
if (message.type == MessageType.speak) {
MessageSpeak messageSpeak = (MessageSpeak) message;
updateMyState(new StringType(messageSpeak.data.utterance));
}
}
@Override
public void handleCommand(Command command) {
if (command instanceof StringType) {
if (handler.sendMessage(new MessageSpeak(command.toFullString()))) {
updateMyState(new StringType(command.toFullString()));
}
}
}
}

View File

@ -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<StringType> {
public UtteranceChannel(MycroftHandler handler) {
super(handler, MycroftBindingConstants.UTTERANCE_CHANNEL);
}
@Override
protected List<MessageType> getMessageToListenTo() {
return Arrays.asList(MessageType.recognizer_loop__utterance);
}
@Override
public void messageReceived(BaseMessage message) {
if (message.type == MessageType.recognizer_loop__utterance) {
List<String> utterances = ((MessageRecognizerLoopUtterance) message).data.utterances;
if (!utterances.isEmpty()) {
updateMyState(new StringType(utterances.get(0)));
}
}
}
@Override
public void handleCommand(Command command) {
if (command instanceof StringType) {
if (handler.sendMessage(new MessageRecognizerLoopUtterance(command.toFullString()))) {
updateMyState(new StringType(command.toFullString()));
}
}
}
}

View File

@ -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<State> {
/**
* As the MessageVolumeSet is, contrary to the documentation, not listened to by Mycroft,
* we use a workaround and send a message simulating an intent detection
*/
public static final String VOLUME_SETTER_MESSAGE = "{\"type\": \"mycroft-volume.mycroftai:SetVolume\", \"data\": {\"intent_type\": \"mycroft-volume.mycroftai:SetVolume\", \"mycroft_volume_mycroftaiVolume\": \"volume\", \"mycroft_volume_mycroftaiLevel\": \"$$VOLUME\", \"mycroft_volume_mycroftaiTo\": \"to\", \"target\": null, \"confidence\": 0.6000000000000001, \"__tags__\": [{\"match\": \"volume\", \"key\": \"volume\", \"start_token\": 1, \"entities\": [{\"key\": \"volume\", \"match\": \"volume\", \"data\": [[\"volume\", \"mycroft_volume_mycroftaiVolume\"]], \"confidence\": 1.0}], \"end_token\": 1, \"from_context\": false}, {\"match\": \"$$VOLUME\", \"key\": \"$$VOLUME\", \"start_token\": 3, \"entities\": [{\"key\": \"$$VOLUME\", \"match\": \"$$VOLUME\", \"data\": [[\"$$VOLUME\", \"mycroft_volume_mycroftaiLevel\"]], \"confidence\": 1.0}], \"end_token\": 3, \"from_context\": false}, {\"match\": \"to\", \"key\": \"to\", \"start_token\": 2, \"entities\": [{\"key\": \"to\", \"match\": \"to\", \"data\": [[\"to\", \"mycroft_volume_mycroftaiTo\"]], \"confidence\": 1.0}], \"end_token\": 2, \"from_context\": false}], \"utterance\": \"set volume to $$VOLUME\", \"utterances\": [\"set volume to X\"]}, \"context\": {\"client_name\": \"mycroft_cli\", \"source\": [\"skills\"], \"destination\": \"debug_cli\"}}";
private PercentType lastVolume = new PercentType(50);
private PercentType lastNonZeroVolume = new PercentType(50);
public VolumeChannel(MycroftHandler handler) {
super(handler, MycroftBindingConstants.VOLUME_CHANNEL);
}
@Override
public List<MessageType> getMessageToListenTo() {
// we don't listen to mute/unmute message because duck/unduck seems sufficient
// and we don't want to change state twice for the same event
// but it should be tested on mark I, as volume is handled differently
return Arrays.asList(MessageType.mycroft_volume_get_response, MessageType.mycroft_volume_set,
MessageType.mycroft_volume_increase, MessageType.mycroft_volume_decrease,
MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck);
}
@Override
public void messageReceived(BaseMessage message) {
if (message.type == MessageType.mycroft_volume_get_response) {
float volumeGet = ((MessageVolumeGetResponse) message).data.percent;
updateAndSaveMyState(normalizeVolume(volumeGet));
} else if (message.type == MessageType.mycroft_volume_set) {
float volumeSet = ((MessageVolumeSet) message).data.percent;
updateAndSaveMyState(normalizeVolume(volumeSet));
} else if (message.type == MessageType.mycroft_volume_duck) {
updateAndSaveMyState(new PercentType(0));
} else if (message.type == MessageType.mycroft_volume_unduck) {
updateAndSaveMyState(lastNonZeroVolume);
} else if (message.type == MessageType.mycroft_volume_increase) {
updateAndSaveMyState(normalizeVolume(lastVolume.intValue() + 10));
} else if (message.type == MessageType.mycroft_volume_decrease) {
updateAndSaveMyState(normalizeVolume(lastVolume.intValue() - 10));
}
}
protected final void updateAndSaveMyState(State state) {
if (state instanceof PercentType) {
this.lastVolume = ((PercentType) state);
if (((PercentType) state).intValue() > 0) {
this.lastNonZeroVolume = ((PercentType) state);
}
}
super.updateMyState(state);
}
/**
* Protection method for volume with
* potentially wrong value.
*
* @param volume The requested volume, on a scale from 0 to 100.
* Could be out of bond, then it will be corrected.
* @return A safe volume in PercentType between 0 and 100
*/
private PercentType normalizeVolume(int volume) {
if (volume >= 100) {
return PercentType.HUNDRED;
} else if (volume <= 0) {
return PercentType.ZERO;
} else {
return new PercentType(volume);
}
}
/**
* Protection method for volume with
* potentially wrong value.
*
* @param volume The requested volume, on a scale from 0 to 1.
* @return A safe volume in PercentType between 0 and 100
*/
private PercentType normalizeVolume(float volume) {
if (volume >= 1) {
return PercentType.HUNDRED;
} else if (volume <= 0) {
return PercentType.ZERO;
} else {
return new PercentType(Math.round(volume * 100));
}
}
public float toMycroftVolume(PercentType percentType) {
return Float.valueOf(percentType.intValue());
}
public PercentType computeNewVolume(int valueAdded) {
return new PercentType(lastVolume.intValue() + valueAdded);
}
private boolean sendSetMessage(float volume) {
String messageToSend = VOLUME_SETTER_MESSAGE.replaceAll("\\$\\$VOLUME", Float.valueOf(volume).toString());
return handler.sendMessage(messageToSend);
}
@Override
public void handleCommand(Command command) {
if (command instanceof OnOffType) {
if (command == OnOffType.ON) {
if (sendSetMessage(toMycroftVolume(lastNonZeroVolume))) {
updateAndSaveMyState(lastNonZeroVolume);
}
}
if (command == OnOffType.OFF) {
if (sendSetMessage(0)) {
updateAndSaveMyState(PercentType.ZERO);
}
}
} else if (command instanceof IncreaseDecreaseType) {
if (command == IncreaseDecreaseType.INCREASE) {
if (handler.sendMessage(new MessageVolumeIncrease())) {
updateAndSaveMyState(computeNewVolume(10));
}
}
if (command == IncreaseDecreaseType.DECREASE) {
handler.sendMessage(new MessageVolumeDecrease());
updateAndSaveMyState(computeNewVolume(-10));
}
} else if (command instanceof PercentType) {
sendSetMessage(toMycroftVolume((PercentType) command));
updateAndSaveMyState((PercentType) command);
} else if (command instanceof RefreshType) {
handler.sendMessage(new MessageVolumeGet());
}
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="mycroft" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Mycroft Binding</name>
<description>
Connects to a Mycroft instance in order to receive information from, and send commands to it. Typical
usage includes
triggering Mycroft to listen (as if a wake word was detected), sending text for Mycroft to speak,
reacting on some
specific intent, command skills by faking a spoken utterance, etc.
</description>
</binding:binding>

View File

@ -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)

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="mycroft"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="mycroft">
<label>Mycroft</label>
<description>A Mycroft instance</description>
<channels>
<channel id="listen" typeId="listen-channel"/>
<channel id="speak" typeId="speak-channel"/>
<channel id="utterance" typeId="utterance-channel"/>
<channel id="player" typeId="system.media-control"/>
<channel id="volume" typeId="system.volume"/>
<channel id="volume_mute" typeId="system.mute"/>
<channel id="full_message" typeId="full-message-channel"/>
</channels>
<config-description>
<parameter name="host" type="text" required="true">
<label>Hostname</label>
<description>This is the host to connect to (ip or hostname)</description>
<context>network-address</context>
</parameter>
<parameter name="port" type="integer" required="false" min="1" max="65535">
<label>Port</label>
<description>This is the port to connect to.</description>
<default>8181</default>
</parameter>
<parameter name="volume_restoration_level" type="integer" required="false" min="1" max="100">
<advanced>true</advanced>
<label>Volume Restoration Level</label>
<description>When unmuted, force Mycroft to restore volume to this value</description>
</parameter>
</config-description>
</thing-type>
<channel-type id="listen-channel">
<item-type>Switch</item-type>
<label>Listen State</label>
<description>Switch to ON when Mycroft is listening. Can simulate a wake work detection to trigger the STT.</description>
</channel-type>
<channel-type id="speak-channel">
<item-type>String</item-type>
<label>TTS</label>
<description>The last sentence Mycroft spoke.</description>
</channel-type>
<channel-type id="utterance-channel">
<item-type>String</item-type>
<label>Utterance</label>
<description>The last utterance Mycroft received.</description>
</channel-type>
<channel-type id="full-message-channel" advanced="true">
<item-type>String</item-type>
<label>Full Bus Message</label>
<description>The last full message seen on the Mycroft Bus.</description>
<config-description>
<parameter name="messageTypes" type="text" required="false">
<label>Full Message Channel Filter</label>
<description>The full message channel will be updated on these message types only (comma separated value)</description>
<default>message.type.1,message.type.2</default>
</parameter>
</config-description>
</channel-type>
</thing:thing-descriptions>

View File

@ -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<MessageSpeak> mockListener = Mockito.mock(MycroftMessageListener.class);
ArgumentCaptor<BaseMessage> argCaptorMessage = ArgumentCaptor.forClass(BaseMessage.class);
// given we register any listener
mycroftConnection.registerListener(MessageType.any, mockListener);
// when we send speak message
@SuppressWarnings("null")
String speakMessageJson = new String(
MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8");
mycroftConnection.onMessage(sessionMock, speakMessageJson);
// then message is correctly received by listener
Mockito.verify(mockListener, Mockito.times(1)).baseMessageReceived(ArgumentMatchers.any());
Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture());
assertEquals(argCaptorMessage.getValue().message, speakMessageJson);
}
@Test
public void testSpeakListener() throws IOException {
MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient());
Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234));
mycroftConnection.onConnect(sessionMock);
@SuppressWarnings("unchecked")
MycroftMessageListener<MessageSpeak> mockListener = Mockito.mock(MycroftMessageListener.class);
ArgumentCaptor<MessageSpeak> argCaptorMessage = ArgumentCaptor.forClass(MessageSpeak.class);
// given we register speak listener
mycroftConnection.registerListener(MessageType.speak, mockListener);
// when we send speak message
@SuppressWarnings("null")
String speakMessageJson = new String(
MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8");
mycroftConnection.onMessage(sessionMock, speakMessageJson);
// then message is correctly received by listener
Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture());
assertEquals(argCaptorMessage.getValue().data.utterance, "coucou");
}
}

View File

@ -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"}}

View File

@ -229,6 +229,7 @@
<module>org.openhab.binding.mqtt.generic</module>
<module>org.openhab.binding.mqtt.homeassistant</module>
<module>org.openhab.binding.mqtt.homie</module>
<module>org.openhab.binding.mycroft</module>
<module>org.openhab.binding.myq</module>
<module>org.openhab.binding.mystrom</module>
<module>org.openhab.binding.nanoleaf</module>