From 46913173de1a171bf58863141118eb58ae48f24f Mon Sep 17 00:00:00 2001 From: Gwendal Roulleau Date: Tue, 20 Jul 2021 23:51:09 +0200 Subject: [PATCH] [mycroft] Mycroft binding - Initial contrib (#11033) 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 --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + bundles/org.openhab.binding.mycroft/NOTICE | 13 + bundles/org.openhab.binding.mycroft/README.md | 110 +++++++ bundles/org.openhab.binding.mycroft/pom.xml | 17 ++ .../src/main/feature/feature.xml | 10 + .../internal/MycroftBindingConstants.java | 44 +++ .../internal/MycroftConfiguration.java | 27 ++ .../mycroft/internal/MycroftHandler.java | 249 ++++++++++++++++ .../internal/MycroftHandlerFactory.java | 65 +++++ .../mycroft/internal/api/MessageType.java | 114 ++++++++ .../internal/api/MessageTypeConverter.java | 48 ++++ .../internal/api/MycroftConnection.java | 268 ++++++++++++++++++ .../api/MycroftConnectionListener.java | 35 +++ .../internal/api/MycroftMessageListener.java | 40 +++ .../mycroft/internal/api/dto/BaseMessage.java | 25 ++ .../internal/api/dto/MessageAudioNext.java | 27 ++ .../internal/api/dto/MessageAudioPause.java | 26 ++ .../internal/api/dto/MessageAudioPlay.java | 27 ++ .../internal/api/dto/MessageAudioPrev.java | 27 ++ .../internal/api/dto/MessageAudioResume.java | 27 ++ .../internal/api/dto/MessageAudioStop.java | 26 ++ .../api/dto/MessageAudioTrackInfo.java | 26 ++ .../api/dto/MessageAudioTrackInfoReply.java | 26 ++ .../internal/api/dto/MessageMicListen.java | 26 ++ .../dto/MessageRecognizerLoopRecordBegin.java | 34 +++ .../dto/MessageRecognizerLoopRecordEnd.java | 34 +++ .../dto/MessageRecognizerLoopUtterance.java | 51 ++++ .../internal/api/dto/MessageSpeak.java | 50 ++++ .../api/dto/MessageVolumeDecrease.java | 32 +++ .../internal/api/dto/MessageVolumeDuck.java | 35 +++ .../internal/api/dto/MessageVolumeGet.java | 35 +++ .../api/dto/MessageVolumeGetResponse.java | 33 +++ .../api/dto/MessageVolumeIncrease.java | 32 +++ .../internal/api/dto/MessageVolumeMute.java | 32 +++ .../internal/api/dto/MessageVolumeSet.java | 32 +++ .../internal/api/dto/MessageVolumeUnduck.java | 35 +++ .../internal/api/dto/MessageVolumeUnmute.java | 32 +++ .../internal/channels/AudioPlayerChannel.java | 98 +++++++ .../channels/ChannelCommandHandler.java | 27 ++ .../internal/channels/DuckChannel.java | 68 +++++ .../internal/channels/FullMessageChannel.java | 64 +++++ .../internal/channels/ListenChannel.java | 61 ++++ .../internal/channels/MuteChannel.java | 68 +++++ .../internal/channels/MycroftChannel.java | 70 +++++ .../internal/channels/SpeakChannel.java | 60 ++++ .../internal/channels/UtteranceChannel.java | 63 ++++ .../internal/channels/VolumeChannel.java | 163 +++++++++++ .../main/resources/OH-INF/binding/binding.xml | 13 + .../resources/OH-INF/thing/thing-types.xml | 92 ++++++ .../internal/api/MycroftConnectionTest.java | 112 ++++++++ .../binding/mycroft/internal/api/speak.json | 1 + bundles/pom.xml | 1 + 53 files changed, 2737 insertions(+) create mode 100644 bundles/org.openhab.binding.mycroft/NOTICE create mode 100644 bundles/org.openhab.binding.mycroft/README.md create mode 100644 bundles/org.openhab.binding.mycroft/pom.xml create mode 100644 bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/DuckChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java create mode 100644 bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java create mode 100644 bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json diff --git a/CODEOWNERS b/CODEOWNERS index bd8485467bc63..394101901ece2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -189,6 +189,7 @@ /bundles/org.openhab.binding.mqtt.homeassistant/ @davidgraeff /bundles/org.openhab.binding.mqtt.homie/ @davidgraeff /bundles/org.openhab.binding.myq/ @digitaldan +/bundles/org.openhab.binding.mycroft/ @dalgwen /bundles/org.openhab.binding.mystrom/ @pail23 /bundles/org.openhab.binding.nanoleaf/ @raepple @stefan-hoehn /bundles/org.openhab.binding.neato/ @jjlauterbach diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 7830bf7d35cf6..99db71963d667 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -926,6 +926,11 @@ org.openhab.binding.mqtt.homie ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.mycroft + ${project.version} + org.openhab.addons.bundles org.openhab.binding.mystrom diff --git a/bundles/org.openhab.binding.mycroft/NOTICE b/bundles/org.openhab.binding.mycroft/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.mycroft/README.md b/bundles/org.openhab.binding.mycroft/README.md new file mode 100644 index 0000000000000..2c3ce2312aa59 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/README.md @@ -0,0 +1,110 @@ +# Mycroft Binding + +This binding will connect to Mycroft A.I. in order to control it or react to event 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 Speach service) +- Control the music player +- Control the sound volume of Mycroft +- React to all the aforementioned events ... +- ... And send/receive all other kind of messages on the message bus + + +## Supported Things + +The only thing managed by this binding is a Mycroft instance. + + +## 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, but you could change it if you want. + +``` +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 | + + +## Channels + +The Mycroft thing supports 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, or ask Mycroft to say something | +| utterance | String | The last utterance Mycroft receive, or ask Mycroft something | +| player | Player | The music player Mycroft is currently controlling | +| volume | Dimmer | The volume of the Mycroft speaker. NOT FUNCTIONNAL. [SEE THIS POST TO SEE EVOLUTION](https://community.mycroft.ai/t/openhab-plugin-development-audio-volume-message-types-missing/10576) | +| volume_mute | Switch | Mute the Mycroft speaker | +| volume_duck | Switch | Duck the volume of the Mycroft speaker | +| 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" } +Switch myMycroft_duck "Duck" { channel="mycroft:mycroft:myMycroft:volume_duck" } +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 : + +```perl +sitemap demo label="myMycroft" +{ + Frame label="myMycroft" { + Switch item=myMycroft_mute + Switch item=myMycroft_duck + Slider item=myMycroft_volume + Default item=myMycroft_player + Switch item=myMycroft_listen + Text item=myMycroft_speak + Text item=myMycroft_utterance + Text item=myMycroft_fullmessage + } +} diff --git a/bundles/org.openhab.binding.mycroft/pom.xml b/bundles/org.openhab.binding.mycroft/pom.xml new file mode 100644 index 0000000000000..c06a12e3f4f6c --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.2.0-SNAPSHOT + + + org.openhab.binding.mycroft + + openHAB Add-ons :: Bundles :: Mycroft Binding + + diff --git a/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml b/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml new file mode 100644 index 0000000000000..04d5e3b8ff972 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + openhab-transport-http + mvn:org.openhab.addons.bundles/org.openhab.binding.mycroft/${project.version} + + diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java new file mode 100644 index 0000000000000..611da2a258808 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftBindingConstants.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2021 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 VOLUME_DUCK_CHANNEL = "volume_duck"; + public static final String UTTERANCE_CHANNEL = "utterance"; + public static final String FULL_MESSAGE_CHANNEL = "full_message"; + + // Channel property : + public static final String FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY = "messageTypes"; +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java new file mode 100644 index 0000000000000..6c22b52653522 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftConfiguration.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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; +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java new file mode 100644 index 0000000000000..bf1e9f991f6e7 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandler.java @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2010-2021 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 java.util.concurrent.atomic.AtomicBoolean; + +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.DuckChannel; +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); + + protected final MycroftConnection connection; + private @Nullable ScheduledFuture scheduledFuture; + private MycroftConfiguration config = new MycroftConfiguration(); + private boolean thingDisposing = false; + private AtomicBoolean isStartingWebSocket = new AtomicBoolean(false); + protected Map> mycroftChannels = new HashMap<>(); + + /** The reconnect frequency in case of error */ + private static final int POLL_FREQUENCY_SEC = 10; + private int sometimesSendVolumeRequest = 0; + + public MycroftHandler(Thing thing, WebSocketFactory webSocketFactory) { + super(thing); + String websocketID = thing.getUID().getAsString().replace(':', '-'); + if (websocketID.length() < 4) { + websocketID = "openHAB-mycroft-" + websocketID; + } else if (websocketID.length() > 20) { + websocketID = websocketID.substring(websocketID.length() - 20); + } + this.connection = new MycroftConnection(this, webSocketFactory.createWebSocketClient(websocketID)); + } + + /** + * 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. + * sometimes send a get volume request to fully test the connection / refresh volume. + */ + private void startWebsocket() { + if (thingDisposing) { + return; + } + if (!isStartingWebSocket.compareAndExchange(false, true)) { + try { + 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); + } + } finally { + stopTimer(); + scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + isStartingWebSocket.set(false); + } + } + } + + @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() { + + updateStatus(ThingStatus.UNKNOWN); + + logger.debug("Start initializing mycroft {}", thing.getUID()); + + config = getConfigAs(MycroftConfiguration.class); + + scheduler.execute(() -> { + startWebsocket(); + }); + + registerChannel(new ListenChannel(this)); + registerChannel(new VolumeChannel(this)); + registerChannel(new MuteChannel(this)); + registerChannel(new DuckChannel(this)); + registerChannel(new SpeakChannel(this)); + registerChannel(new AudioPlayerChannel(this)); + registerChannel(new UtteranceChannel(this)); + + final Channel fullMessageChannel = getThing().getChannel(MycroftBindingConstants.FULL_MESSAGE_CHANNEL); + @SuppressWarnings("null") // cannot be null + String messageTypesProperty = (String) fullMessageChannel.getConfiguration() + .get(MycroftBindingConstants.FULL_MESSAGE_CHANNEL_MESSAGE_TYPE_PROPERTY); + + registerChannel(new FullMessageChannel(this, messageTypesProperty)); + + checkLinkedChannelsAndRegisterMessageListeners(); + } + + private void checkLinkedChannelsAndRegisterMessageListeners() { + for (Entry> channelEntry : mycroftChannels.entrySet()) { + ChannelUID uid = channelEntry.getKey(); + MycroftChannel channel = channelEntry.getValue(); + if (isLinked(uid)) { + channel.registerListeners(); + } else { + channel.unregisterListeners(); + } + } + } + + @Override + public void channelLinked(ChannelUID channelUID) { + checkLinkedChannelsAndRegisterMessageListeners(); + } + + @Override + public void channelUnlinked(ChannelUID channelUID) { + checkLinkedChannelsAndRegisterMessageListeners(); + } + + private void registerChannel(MycroftChannel channel) { + mycroftChannels.put(channel.getChannelUID(), channel); + } + + public void registerMessageListener(MessageType messageType, MycroftMessageListener listener) { + this.connection.registerListener(messageType, listener); + } + + public void unregisterMessageListener(MessageType messageType, MycroftMessageListener listener) { + this.connection.unregisterListener(messageType, listener); + } + + @Override + public void connectionEstablished() { + stopTimer(); + logger.debug("Mycroft thing {} is online", thing.getUID()); + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void connectionLost(String reason) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, reason); + + stopTimer(); + // Wait for POLL_FREQUENCY_SEC after a connection was closed before trying again + scheduledFuture = scheduler.schedule(this::startWebsocket, POLL_FREQUENCY_SEC, TimeUnit.SECONDS); + } + + @Override + public void dispose() { + thingDisposing = true; + stopTimer(); + connection.close(); + } + + public void updateMyChannel(MycroftChannel mycroftChannel, T state) { + updateState(mycroftChannel.getChannelUID(), state); + } + + public boolean sendMessage(BaseMessage message) { + try { + connection.sendMessage(message); + return true; + } catch (IOException e) { + logger.warn("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.warn("Cannot send message of type {}, for reason {}", message.getClass().getName(), e.getMessage()); + return false; + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java new file mode 100644 index 0000000000000..ed2fd2ccf2125 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/MycroftHandlerFactory.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + * + * @author Gwendal ROULLEAU - Initial contribution + */ +package org.openhab.binding.mycroft.internal; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.io.net.http.WebSocketFactory; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MycroftHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.mycroft", service = ThingHandlerFactory.class) +public class MycroftHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(MycroftBindingConstants.MYCROFT); + + private final WebSocketFactory webSocketFactory; + + @Activate + public MycroftHandlerFactory(final @Reference WebSocketFactory webSocketFactory) { + this.webSocketFactory = webSocketFactory; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (MycroftBindingConstants.MYCROFT.equals(thingTypeUID)) { + return new MycroftHandler(thing, webSocketFactory); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java new file mode 100644 index 0000000000000..fe4378a503aa5 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageType.java @@ -0,0 +1,114 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.api; + +import java.util.stream.Stream; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioNext; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPause; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPlay; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioPrev; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioResume; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioStop; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfo; +import org.openhab.binding.mycroft.internal.api.dto.MessageAudioTrackInfoReply; +import org.openhab.binding.mycroft.internal.api.dto.MessageMicListen; +import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordBegin; +import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopRecordEnd; +import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopUtterance; +import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDecrease; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeDuck; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGet; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeGetResponse; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeIncrease; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeMute; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeSet; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnduck; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnmute; + +/** + * All message type of interest, issued by Mycroft, are referenced here + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public enum MessageType { + + any("special-anymessages", BaseMessage.class), + speak("speak", MessageSpeak.class), + recognizer_loop__record_begin("recognizer_loop:record_begin", MessageRecognizerLoopRecordBegin.class), + recognizer_loop__record_end("recognizer_loop:record_end", MessageRecognizerLoopRecordEnd.class), + recognizer_loop__utterance("recognizer_loop:utterance", MessageRecognizerLoopUtterance.class), + mycroft_mic_listen("mycroft.mic.listen", MessageMicListen.class), + + mycroft_audio_service_pause("mycroft.audio.service.pause", MessageAudioPause.class), + mycroft_audio_service_resume("mycroft.audio.service.resume", MessageAudioResume.class), + mycroft_audio_service_stop("mycroft.audio.service.stop", MessageAudioStop.class), + mycroft_audio_service_play("mycroft.audio.service.play", MessageAudioPlay.class), + mycroft_audio_service_next("mycroft.audio.service.next", MessageAudioNext.class), + mycroft_audio_service_prev("mycroft.audio.service.prev", MessageAudioPrev.class), + mycroft_audio_service_track_info("mycroft.audio.service.track_info", MessageAudioTrackInfo.class), + mycroft_audio_service_track_info_reply("mycroft.audio.service.track_info_reply", MessageAudioTrackInfoReply.class), + + mycroft_volume_set("mycroft.volume.set", MessageVolumeSet.class), + mycroft_volume_increase("mycroft.volume.increase", MessageVolumeIncrease.class), + mycroft_volume_decrease("mycroft.volume.decrease", MessageVolumeDecrease.class), + mycroft_volume_get("mycroft.volume.get", MessageVolumeGet.class), + mycroft_volume_get_response("mycroft.volume.get.response", MessageVolumeGetResponse.class), + mycroft_volume_mute("mycroft.volume.mute", MessageVolumeMute.class), + mycroft_volume_unmute("mycroft.volume.unmute", MessageVolumeUnmute.class), + mycroft_volume_duck("mycroft.volume.duck", MessageVolumeDuck.class), + mycroft_volume_unduck("mycroft.volume.unduck", MessageVolumeUnduck.class), + + mycroft_reminder_mycroftai__reminder("mycroft-reminder.mycroftai:reminder", BaseMessage.class), + mycroft_date_time_mycroftai__timeskillupdate_display("mycroft-date-time.mycroftai:TimeSkillupdate_display", + BaseMessage.class), + mycroft_configuration_mycroftai__configurationskillupdate_remote( + "mycroft-configuration.mycroftai:ConfigurationSkillupdate_remote", BaseMessage.class); + + private @NotNull Class messageTypeClass; + private @NotNull String messageTypeName; + + MessageType(String messageTypeName, Class messageType) { + this.messageTypeClass = messageType; + this.messageTypeName = messageTypeName; + } + + /** + * get the expected message type for this message type + * + * @return + */ + public @NotNull Class getMessageTypeClass() { + return messageTypeClass; + } + + @NotNull + public static MessageType fromString(String asString) { + return Stream.of(values()).filter(messageType -> messageType.messageTypeName.equals(asString)).findFirst() + .orElse(any); + } + + public String getMessageTypeName() { + return messageTypeName; + } + + protected void setMessageTypeName(String messageTypeName) { + this.messageTypeName = messageTypeName; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java new file mode 100644 index 0000000000000..11a8800bb3ed4 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MessageTypeConverter.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2010-2021 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 for {@link LightType} + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public class MessageTypeConverter implements JsonDeserializer, JsonSerializer { + @Override + public @Nullable MessageType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + MessageType messageType = MessageType.fromString(json.getAsString()); + // for message of type non recognized : + messageType.setMessageTypeName(json.getAsString()); + return messageType; + } + + @Override + public JsonElement serialize(MessageType src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.getMessageTypeName()); + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java new file mode 100644 index 0000000000000..8bce75a2b9800 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnection.java @@ -0,0 +1,268 @@ +/** + * Copyright (c) 2010-2021 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 org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * Establishes and keeps a websocket connection to the Mycroft bus + * + * @author Gwendal ROULLEAU - Initial contribution. Inspired by the deconz binding. + */ +@WebSocket +@NonNullByDefault +public class MycroftConnection { + private static final AtomicInteger INSTANCE_COUNTER = new AtomicInteger(); + private final Logger logger = LoggerFactory.getLogger(MycroftConnection.class); + + private final WebSocketClient client; + private final String socketName; + private final Gson gson; + + private final MycroftConnectionListener connectionListener; + private final Map>> listeners = new ConcurrentHashMap<>(); + + private ConnectionState connectionState = ConnectionState.DISCONNECTED; + private @Nullable Session session; + + private static final int TIMEOUT_MILLISECONDS = 3000; + + public MycroftConnection(MycroftConnectionListener listener, WebSocketClient client) { + this.connectionListener = listener; + this.client = client; + this.client.setConnectTimeout(TIMEOUT_MILLISECONDS); + this.client.setMaxIdleTimeout(0); + this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet(); + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(MessageType.class, new MessageTypeConverter()); + gson = gsonBuilder.create(); + } + + public MycroftConnection(MycroftConnectionListener listener) { + this.connectionListener = listener; + this.client = new WebSocketClient(); + this.client.setMaxIdleTimeout(0); + this.client.setConnectTimeout(TIMEOUT_MILLISECONDS); + this.socketName = "Websocket$" + System.currentTimeMillis() + "-" + INSTANCE_COUNTER.incrementAndGet(); + + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(MessageType.class, new MessageTypeConverter()); + gson = gsonBuilder.create(); + } + + public void start(String ip, int port) { + if (connectionState == ConnectionState.CONNECTED) { + return; + } else if (connectionState == ConnectionState.CONNECTING) { + logger.debug("{} already connecting", socketName); + return; + } else if (connectionState == ConnectionState.DISCONNECTING) { + logger.warn("{} trying to re-connect while still disconnecting", socketName); + } + Future futureConnect = null; + try { + URI destUri = URI.create("ws://" + ip + ":" + port + "/core"); + client.start(); + logger.debug("Trying to connect {} to {}", socketName, destUri); + futureConnect = client.connect(this, destUri); + futureConnect.get(TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS); + } catch (Exception e) { + if (futureConnect != null) { + futureConnect.cancel(true); + } + connectionListener + .connectionLost("Error while connecting: " + (e.getMessage() != null ? e.getMessage() : "unknown")); + } + } + + public void close() { + try { + connectionState = ConnectionState.DISCONNECTING; + client.stop(); + } catch (Exception e) { + logger.debug("{} encountered an error while closing connection", socketName, e); + } + client.destroy(); + } + + /** + * The listener registered in this method will be called when a corresponding message will be detected + * on the mycroft bus + * + * @param messageType + * @param listener + */ + public void registerListener(MessageType messageType, MycroftMessageListener listener) { + Set> messageTypeListeners = listeners.get(messageType); + if (messageTypeListeners == null) { + messageTypeListeners = new HashSet>(); + listeners.put(messageType, messageTypeListeners); + } + messageTypeListeners.add(listener); + } + + public void unregisterListener(MessageType messageType, MycroftMessageListener listener) { + Optional.ofNullable(listeners.get(messageType)) + .ifPresent((messageTypeListeners) -> messageTypeListeners.remove(listener)); + } + + public void sendMessage(BaseMessage message) throws IOException { + sendMessage(gson.toJson(message)); + } + + public void sendMessage(String message) throws IOException { + final Session storedSession = this.session; + try { + if (storedSession != null) { + storedSession.getRemote().sendString(message); + } else { + throw new IOException("Session is not initialized"); + } + } catch (IOException e) { + if (storedSession != null && storedSession.isOpen()) { + storedSession.close(-1, "Sending message error"); + } + throw e; + } + } + + @OnWebSocketConnect + public void onConnect(Session session) { + connectionState = ConnectionState.CONNECTED; + logger.debug("{} successfully connected to {}: {}", socketName, session.getRemoteAddress().getAddress(), + session.hashCode()); + connectionListener.connectionEstablished(); + this.session = session; + } + + @OnWebSocketMessage + public void onMessage(Session session, String message) { + if (!session.equals(this.session)) { + handleWrongSession(session, message); + return; + } + logger.trace("{} received raw data: {}", socketName, message); + + try { + + // listeners on message type : + BaseMessage mycroftMessage = gson.fromJson(message, BaseMessage.class); + Objects.requireNonNull(mycroftMessage); + mycroftMessage.message = message; + + // special listener : to any messages + Set> listenersAnyToNotify = listeners.get(MessageType.any); + if (listenersAnyToNotify != null && !listenersAnyToNotify.isEmpty()) { + for (MycroftMessageListener listener : listenersAnyToNotify) { + listener.baseMessageReceived(mycroftMessage); + } + } + + // listener for specific message type + Set> listenersToNotify = listeners.get(mycroftMessage.type); + if (mycroftMessage.type != MessageType.any && listenersToNotify != null && !listenersToNotify.isEmpty()) { + BaseMessage fullMycroftMessage = gson.fromJson(message, mycroftMessage.type.getMessageTypeClass()); + mycroftMessage.message = message; + Objects.requireNonNull(fullMycroftMessage); + listenersToNotify.forEach(listener -> listener.baseMessageReceived(fullMycroftMessage)); + } + + } catch (RuntimeException e) { + // we need to catch all processing exceptions, otherwise they could affect the connection + logger.warn("{} 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.warn("{} 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.warn("received and discarded message for null session : {}", message); + } else { + logger.warn("{} received and discarded message for other session {}: {}.", socketName, session.hashCode(), + message); + } + } + + /** + * check connection state (successfully connected) + * + * @return true if connected, false if connecting, disconnecting or disconnected + */ + public boolean isConnected() { + return connectionState == ConnectionState.CONNECTED; + } + + /** + * used internally to represent the connection state + */ + private enum ConnectionState { + CONNECTING, + CONNECTED, + DISCONNECTING, + DISCONNECTED + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java new file mode 100644 index 0000000000000..c6256a39e6344 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionListener.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Informs about the websocket connection. + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public interface MycroftConnectionListener { + /** + * Connection successfully established. + */ + void connectionEstablished(); + + /** + * Connection lost. A reconnect timer has been started. + * + * @param reason A reason for the disconnection + */ + void connectionLost(String reason); +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java new file mode 100644 index 0000000000000..f8d549cf62246 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/MycroftMessageListener.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; + +/** + * Informs about received messages + * + * @author Gwendal Roulleau - Initial contribution + */ +@NonNullByDefault +public interface MycroftMessageListener { + /** + * A new message was received + * + * @param message The received message + */ + void messageReceived(T message); + + @SuppressWarnings("unchecked") + default void baseMessageReceived(BaseMessage baseMessage) { + try { + messageReceived(((T) baseMessage)); + } catch (ClassCastException cce) { + throw new ClassCastException("Incorrect use of message in Mycroft binding"); + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java new file mode 100644 index 0000000000000..649cdc96ca4d7 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/BaseMessage.java @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class BaseMessage { + + public MessageType type; + public String message = ""; +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java new file mode 100644 index 0000000000000..062316c9f0c3f --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioNext.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial Contribution + * + */ +public class MessageAudioNext extends BaseMessage { + + public MessageAudioNext() { + this.type = MessageType.mycroft_audio_service_next; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java new file mode 100644 index 0000000000000..27c614944088f --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPause.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageAudioPause extends BaseMessage { + + public MessageAudioPause() { + this.type = MessageType.mycroft_audio_service_pause; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java new file mode 100644 index 0000000000000..b9f1e0335980a --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPlay.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + * + */ +public class MessageAudioPlay extends BaseMessage { + + public MessageAudioPlay() { + this.type = MessageType.mycroft_audio_service_play; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java new file mode 100644 index 0000000000000..76d03fabdb8a3 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioPrev.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + * + */ +public class MessageAudioPrev extends BaseMessage { + + public MessageAudioPrev() { + this.type = MessageType.mycroft_audio_service_prev; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java new file mode 100644 index 0000000000000..9de316e87dc61 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioResume.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + * + */ +public class MessageAudioResume extends BaseMessage { + + public MessageAudioResume() { + this.type = MessageType.mycroft_audio_service_resume; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java new file mode 100644 index 0000000000000..17d42dd3f48ab --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioStop.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageAudioStop extends BaseMessage { + + public MessageAudioStop() { + this.type = MessageType.mycroft_audio_service_stop; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java new file mode 100644 index 0000000000000..8415f8ad022e5 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfo.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageAudioTrackInfo extends BaseMessage { + + public MessageAudioTrackInfo() { + this.type = MessageType.mycroft_audio_service_track_info; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java new file mode 100644 index 0000000000000..1ea96c6891f7b --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageAudioTrackInfoReply.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageAudioTrackInfoReply extends BaseMessage { + + public MessageAudioTrackInfoReply() { + this.type = MessageType.mycroft_audio_service_track_info_reply; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java new file mode 100644 index 0000000000000..da2267cc47849 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageMicListen.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageMicListen extends BaseMessage { + + public MessageMicListen() { + this.type = MessageType.mycroft_mic_listen; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java new file mode 100644 index 0000000000000..28468bbcc5518 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordBegin.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageRecognizerLoopRecordBegin extends BaseMessage { + + public Context context = new Context(); + + public MessageRecognizerLoopRecordBegin() { + this.type = MessageType.recognizer_loop__record_begin; + } + + public static class Context { + public String client_name = ""; + public String source = ""; + public String destination = ""; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java new file mode 100644 index 0000000000000..abf92e9e293b9 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopRecordEnd.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageRecognizerLoopRecordEnd extends BaseMessage { + + public Context context = new Context(); + + public MessageRecognizerLoopRecordEnd() { + this.type = MessageType.recognizer_loop__record_end; + } + + public static class Context { + public String client_name = ""; + public String source = ""; + public String destination = ""; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java new file mode 100644 index 0000000000000..1937af97e7fab --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageRecognizerLoopUtterance.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageRecognizerLoopUtterance extends BaseMessage { + + public Data data = new Data(); + + public Context context = new Context(); + + public MessageRecognizerLoopUtterance() { + this.type = MessageType.recognizer_loop__utterance; + } + + public MessageRecognizerLoopUtterance(String utterance) { + this(); + this.data.utterances.add(utterance); + this.context.client_name = "java_api"; + this.context.source = "audio"; + this.context.destination.add("skills"); + } + + public static class Data { + public List utterances = new ArrayList<>(); + } + + public static class Context { + public String client_name = ""; + public String source = ""; + public List destination = new ArrayList<>(); + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java new file mode 100644 index 0000000000000..e307e3d8c1996 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageSpeak.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageSpeak extends BaseMessage { + + public Data data = new Data(); + + public Context context = new Context(); + + public MessageSpeak() { + this.type = MessageType.speak; + } + + public MessageSpeak(String textToSay) { + this(); + this.data = new Data(); + this.data.utterance = textToSay; + } + + public static class Data { + public String utterance = ""; + public String expect_response = ""; + }; + + public static class Context { + public String client_name = ""; + public List source = new ArrayList<>(); + public String destination = ""; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java new file mode 100644 index 0000000000000..f2f4239c64f49 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDecrease.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeDecrease extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeDecrease() { + this.type = MessageType.mycroft_volume_decrease; + } + + public static class Data { + public Boolean play_sound = true; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java new file mode 100644 index 0000000000000..cce27468e9f24 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeDuck.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeDuck extends BaseMessage { + + public Data data = new Data(); + public Context context = new Context(); + + public MessageVolumeDuck() { + this.type = MessageType.mycroft_volume_duck; + } + + public static final class Data { + } + + public static final class Context { + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java new file mode 100644 index 0000000000000..8ef06cce84916 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGet.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeGet extends BaseMessage { + + public Data data = new Data(); + public Context context = new Context(); + + public MessageVolumeGet() { + this.type = MessageType.mycroft_volume_get; + } + + public static final class Data { + } + + public static final class Context { + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java new file mode 100644 index 0000000000000..6792e5c68f8ec --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeGetResponse.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeGetResponse extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeGetResponse() { + this.type = MessageType.mycroft_volume_get_response; + } + + public static class Data { + public float percent = 0; + public Boolean muted = false; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java new file mode 100644 index 0000000000000..466208e283b61 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeIncrease.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeIncrease extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeIncrease() { + this.type = MessageType.mycroft_volume_increase; + } + + public static class Data { + public Boolean play_sound = true; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java new file mode 100644 index 0000000000000..e28b631287ba2 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeMute.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeMute extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeMute() { + this.type = MessageType.mycroft_volume_mute; + } + + public static class Data { + public Boolean speak_message = false; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java new file mode 100644 index 0000000000000..9872bf77b30f9 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeSet.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeSet extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeSet() { + this.type = MessageType.mycroft_volume_set; + } + + public static class Data { + public float percent = 0; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java new file mode 100644 index 0000000000000..eb3ca059bc7f8 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnduck.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeUnduck extends BaseMessage { + + public Data data = new Data(); + public Context context = new Context(); + + public MessageVolumeUnduck() { + this.type = MessageType.mycroft_volume_unduck; + } + + public static final class Data { + } + + public static final class Context { + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java new file mode 100644 index 0000000000000..79525eba55e2d --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/api/dto/MessageVolumeUnmute.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2021 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; + +/** + * + * @author Gwendal ROULLEAU - Initial contribution + */ +public class MessageVolumeUnmute extends BaseMessage { + + public Data data = new Data(); + + public MessageVolumeUnmute() { + this.type = MessageType.mycroft_volume_unmute; + } + + public static class Data { + public Boolean speak_message = false; + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java new file mode 100644 index 0000000000000..d69429f711d9d --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/AudioPlayerChannel.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010-2021 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 handle the Mycroft capability to act as a music player + * (depending on common play music skills installed) + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public class AudioPlayerChannel extends MycroftChannel { + + public AudioPlayerChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.PLAYER_CHANNEL); + } + + @Override + protected List getMessageToListenTo() { + return Arrays.asList(MessageType.mycroft_audio_service_prev, MessageType.mycroft_audio_service_next, + MessageType.mycroft_audio_service_pause, MessageType.mycroft_audio_service_resume, + MessageType.mycroft_audio_service_play, MessageType.mycroft_audio_service_stop, + MessageType.mycroft_audio_service_track_info, MessageType.mycroft_audio_service_track_info_reply); + } + + @Override + public void messageReceived(BaseMessage message) { + switch (message.type) { + case mycroft_audio_service_pause: + case mycroft_audio_service_stop: + updateMyState(PlayPauseType.PAUSE); + break; + case mycroft_audio_service_play: + case mycroft_audio_service_resume: + updateMyState(PlayPauseType.PLAY); + break; + default: + break; + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof PlayPauseType) { + if (((PlayPauseType) command) == PlayPauseType.PAUSE) { + if (handler.sendMessage(new MessageAudioPause())) { + updateMyState(PlayPauseType.PAUSE); + } + } + if (((PlayPauseType) command) == PlayPauseType.PLAY) { + handler.sendMessage(new MessageAudioPlay()); + if (handler.sendMessage(new MessageAudioResume())) { + updateMyState(PlayPauseType.PLAY); + } + } + } + if (command instanceof NextPreviousType) { + if (((NextPreviousType) command) == NextPreviousType.NEXT) { + if (handler.sendMessage(new MessageAudioNext())) { + updateMyState(PlayPauseType.PLAY); + } + } + if (((NextPreviousType) command) == NextPreviousType.PREVIOUS) { + if (handler.sendMessage(new MessageAudioPrev())) { + updateMyState(PlayPauseType.PLAY); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java new file mode 100644 index 0000000000000..98831fbd23a81 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ChannelCommandHandler.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.Command; + +/** + * Interface for channel which can handle command + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public interface ChannelCommandHandler { + + public void handleCommand(Command command); +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/DuckChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/DuckChannel.java new file mode 100644 index 0000000000000..22e95cd24ab51 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/DuckChannel.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2021 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.MessageVolumeDuck; +import org.openhab.binding.mycroft.internal.api.dto.MessageVolumeUnduck; +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 DuckChannel extends MycroftChannel { + + public DuckChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.VOLUME_DUCK_CHANNEL); + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.mycroft_volume_duck, MessageType.mycroft_volume_unduck); + } + + @Override + public void messageReceived(BaseMessage message) { + if (message.type == MessageType.mycroft_volume_duck) { + updateMyState(OnOffType.ON); + } else if (message.type == MessageType.mycroft_volume_unduck) { + updateMyState(OnOffType.OFF); + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + if (handler.sendMessage(new MessageVolumeDuck())) { + updateMyState(OnOffType.ON); + } + } else if (command == OnOffType.OFF) { + if (handler.sendMessage(new MessageVolumeUnduck())) { + updateMyState(OnOffType.OFF); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java new file mode 100644 index 0000000000000..55ceab488f621 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/FullMessageChannel.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.MycroftBindingConstants; +import org.openhab.binding.mycroft.internal.MycroftHandler; +import org.openhab.binding.mycroft.internal.api.MessageType; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; + +/** + * The channel responsible for sending/receiving raw message + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public class FullMessageChannel extends MycroftChannel { + + private List messageTypesList = new ArrayList<>(); + + public FullMessageChannel(MycroftHandler handler, String messageTypesList) { + super(handler, MycroftBindingConstants.FULL_MESSAGE_CHANNEL); + for (String messageType : messageTypesList.split(",")) { + this.messageTypesList.add(messageType.trim()); + } + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.any); + } + + @Override + public void messageReceived(BaseMessage message) { + if (messageTypesList.contains(message.type.getMessageTypeName())) { + updateMyState(new StringType(message.message)); + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof StringType) { + if (handler.sendMessage(command.toString())) { + updateMyState(new StringType(command.toString())); + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java new file mode 100644 index 0000000000000..ba654dd650574 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/ListenChannel.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.MycroftBindingConstants; +import org.openhab.binding.mycroft.internal.MycroftHandler; +import org.openhab.binding.mycroft.internal.api.MessageType; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.binding.mycroft.internal.api.dto.MessageMicListen; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.types.Command; + +/** + * The channel responsible for triggering STT recognition + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public class ListenChannel extends MycroftChannel { + + public ListenChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.LISTEN_CHANNEL); + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.recognizer_loop__record_begin, MessageType.recognizer_loop__record_end); + } + + @Override + public void messageReceived(BaseMessage message) { + if (message.type == MessageType.recognizer_loop__record_begin) { + updateMyState(OnOffType.ON); + } else if (message.type == MessageType.recognizer_loop__record_end) { + updateMyState(OnOffType.OFF); + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + handler.sendMessage(new MessageMicListen()); + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java new file mode 100644 index 0000000000000..f99aa1150c020 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MuteChannel.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010-2021 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.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 { + + public MuteChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.VOLUME_MUTE_CHANNEL); + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.mycroft_volume_mute, MessageType.mycroft_volume_unmute); + } + + @Override + public void messageReceived(BaseMessage message) { + if (message.type == MessageType.mycroft_volume_mute) { + updateMyState(OnOffType.ON); + } else if (message.type == MessageType.mycroft_volume_unmute) { + updateMyState(OnOffType.OFF); + } + } + + @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); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java new file mode 100644 index 0000000000000..b8452d535b0a6 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/MycroftChannel.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.MycroftHandler; +import org.openhab.binding.mycroft.internal.api.MessageType; +import org.openhab.binding.mycroft.internal.api.MycroftMessageListener; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.types.State; + +/** + * A helper method for channel handling + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public abstract class MycroftChannel + implements ChannelCommandHandler, MycroftMessageListener { + + private ChannelUID channelUID; + protected MycroftHandler handler; + + public MycroftChannel(MycroftHandler handler, String channelUIDPart) { + this.handler = handler; + this.channelUID = new ChannelUID(handler.getThing().getUID(), channelUIDPart); + } + + public final ChannelUID getChannelUID() { + return channelUID; + } + + protected final void updateMyState(T state) { + handler.updateMyChannel(this, state); + } + + public final void registerListeners() { + for (MessageType messageType : getMessageToListenTo()) { + handler.registerMessageListener(messageType, this); + } + } + + protected List getMessageToListenTo() { + return new ArrayList<>(); + } + + public final void unregisterListeners() { + for (MessageType messageType : getMessageToListenTo()) { + handler.unregisterMessageListener(messageType, this); + } + } + + @Override + public void messageReceived(BaseMessage message) { + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java new file mode 100644 index 0000000000000..2d4c8a9ebe770 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/SpeakChannel.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.MycroftBindingConstants; +import org.openhab.binding.mycroft.internal.MycroftHandler; +import org.openhab.binding.mycroft.internal.api.MessageType; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; + +/** + * The channel responsible for TSS + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public class SpeakChannel extends MycroftChannel { + + public SpeakChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.SPEAK_CHANNEL); + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.speak); + } + + @Override + public void messageReceived(BaseMessage message) { + if (message.type == MessageType.speak) { + MessageSpeak messageSpeak = (MessageSpeak) message; + updateMyState(new StringType(messageSpeak.data.utterance)); + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof StringType) { + if (handler.sendMessage(new MessageSpeak(command.toString()))) { + updateMyState(new StringType(command.toString())); + } + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java new file mode 100644 index 0000000000000..6682c92ea6428 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/UtteranceChannel.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.channels; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.mycroft.internal.MycroftBindingConstants; +import org.openhab.binding.mycroft.internal.MycroftHandler; +import org.openhab.binding.mycroft.internal.api.MessageType; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.binding.mycroft.internal.api.dto.MessageRecognizerLoopUtterance; +import org.openhab.core.library.types.StringType; +import org.openhab.core.types.Command; + +/** + * This channel handle the full utterance send or received by Mycroft, before any intent recognition + * + * @author Gwendal ROULLEAU - Initial contribution + * + */ +@NonNullByDefault +public class UtteranceChannel extends MycroftChannel { + + public UtteranceChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.UTTERANCE_CHANNEL); + } + + @Override + protected List getMessageToListenTo() { + return Arrays.asList(MessageType.recognizer_loop__utterance); + } + + @Override + public void messageReceived(BaseMessage message) { + if (message.type == MessageType.recognizer_loop__utterance) { + List utterances = ((MessageRecognizerLoopUtterance) message).data.utterances; + if (!utterances.isEmpty()) { + updateMyState(new StringType(utterances.get(0))); + } + } + } + + @Override + public void handleCommand(Command command) { + if (command instanceof StringType) { + MessageRecognizerLoopUtterance utteranceMessage = new MessageRecognizerLoopUtterance(command.toString()); + handler.sendMessage(utteranceMessage); + updateMyState(new StringType(command.toString())); + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java new file mode 100644 index 0000000000000..591d07b7b66dd --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/java/org/openhab/binding/mycroft/internal/channels/VolumeChannel.java @@ -0,0 +1,163 @@ +/** + * Copyright (c) 2010-2021 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 + * + * @author Gwendal ROULLEAU - Initial contribution + */ +@NonNullByDefault +public class VolumeChannel extends MycroftChannel { + + private PercentType lastVolume = new PercentType(50); + private PercentType lastNonZeroVolume = new PercentType(50); + + public VolumeChannel(MycroftHandler handler) { + super(handler, MycroftBindingConstants.VOLUME_CHANNEL); + } + + @Override + public List getMessageToListenTo() { + return Arrays.asList(MessageType.mycroft_volume_get_response, MessageType.mycroft_volume_set, + MessageType.mycroft_volume_mute, MessageType.mycroft_volume_unmute, MessageType.mycroft_volume_increase, + MessageType.mycroft_volume_decrease); + } + + @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_mute) { + updateAndSaveMyState(new PercentType(0)); + } else if (message.type == MessageType.mycroft_volume_unmute) { + 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); + } + + /** + * Volume between 0 and 100 + * + * @param volume + * @return + */ + private PercentType normalizeVolume(int volume) { + if (volume >= 100) { + return PercentType.HUNDRED; + } else if (volume <= 0) { + return PercentType.ZERO; + } else { + return new PercentType(volume); + } + } + + /** + * Volume between 0 and 1 + * + * @param volume + * @return + */ + 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() / 100f); + } + + public PercentType computeNewVolume(int valueAdded) { + return new PercentType(lastVolume.intValue() + valueAdded); + } + + @Override + public void handleCommand(Command command) { + if (command instanceof OnOffType) { + if (command == OnOffType.ON) { + MessageVolumeSet messageVolumeSet = new MessageVolumeSet(); + messageVolumeSet.data.percent = toMycroftVolume(lastNonZeroVolume); + if (handler.sendMessage(messageVolumeSet)) { + updateAndSaveMyState(lastNonZeroVolume); + } + } + if (command == OnOffType.OFF) { + MessageVolumeSet messageVolumeSet = new MessageVolumeSet(); + messageVolumeSet.data.percent = 0; + if (handler.sendMessage(messageVolumeSet)) { + 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) { + MessageVolumeSet messageVolumeSet = new MessageVolumeSet(); + messageVolumeSet.data.percent = toMycroftVolume((PercentType) command); + handler.sendMessage(messageVolumeSet); + updateAndSaveMyState((PercentType) command); + } else if (command instanceof RefreshType) { + handler.sendMessage(new MessageVolumeGet()); + } + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..1479265170236 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,13 @@ + + + + Mycroft Binding + Connect to the Mycroft message bus in order to receive information from, and send command to Mycroft. + 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. + Gwendal ROULLEAU + + diff --git a/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..cf5b4b2db4b54 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,92 @@ + + + + + + + A Mycroft instance + + + + + + + + + + + + + + + + This is the host to connect to (ip or hostname) + + + + This is the port to connect to. + 8181 + + + + + + + Switch + + Switch to ON when Mycroft is listening. Can simulate a wake work detection to trigger the STT. + + + + String + + The last sentence Mycroft speaks, or ask Mycroft to say something. + + + + String + + The last utterance Mycroft receive, or ask Mycroft something. + + + + String + + The last full message seen on the Mycroft Bus. + + + + The full message channel will be updated on these message types only (comma separated value) + message.type.1,message.type.2 + + + + + + Player + + The music player Mycroft is currently controlling. + + + + Dimmer + + The volume of the Mycroft speaker + + + + Switch + + Mute the Mycroft speaker + + + + Switch + + Duck the volume of the Mycroft speaker + + + diff --git a/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java b/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java new file mode 100644 index 0000000000000..b1bca2f524d7c --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/test/java/org/openhab/binding/mycroft/internal/api/MycroftConnectionTest.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2021 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.mycroft.internal.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.binding.mycroft.internal.api.dto.BaseMessage; +import org.openhab.binding.mycroft.internal.api.dto.MessageSpeak; + +/** + * This class provides tests for mycroft binding + * + * @author Gwendal Roulleau - Initial contribution + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.WARN) +@NonNullByDefault +public class MycroftConnectionTest { + + private @Mock @NonNullByDefault({}) MycroftConnectionListener mycroftConnectionListener; + private @Mock @NonNullByDefault({}) Session sessionMock; + + @Test + public void testConnectionOK() throws IOException { + + MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient()); + Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234)); + mycroftConnection.onConnect(sessionMock); + + Mockito.verify(mycroftConnectionListener, Mockito.times(1)).connectionEstablished(); + } + + @Test + public void testAnyListener() throws UnsupportedEncodingException, IOException { + MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient()); + + Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234)); + mycroftConnection.onConnect(sessionMock); + + @SuppressWarnings("unchecked") + MycroftMessageListener mockListener = Mockito.mock(MycroftMessageListener.class); + ArgumentCaptor argCaptorMessage = ArgumentCaptor.forClass(BaseMessage.class); + + // given we register any listener + mycroftConnection.registerListener(MessageType.any, mockListener); + + // when we send speak message + @SuppressWarnings("null") + String speakMessageJson = new String( + MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8"); + mycroftConnection.onMessage(sessionMock, speakMessageJson); + + // then message is correctly received by listener + Mockito.verify(mockListener, Mockito.times(1)).baseMessageReceived(ArgumentMatchers.any()); + Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture()); + + assertEquals(argCaptorMessage.getValue().message, speakMessageJson); + } + + @Test + public void testSpeakListener() throws IOException { + + MycroftConnection mycroftConnection = new MycroftConnection(mycroftConnectionListener, new WebSocketClient()); + + Mockito.when(sessionMock.getRemoteAddress()).thenReturn(new InetSocketAddress(1234)); + mycroftConnection.onConnect(sessionMock); + + @SuppressWarnings("unchecked") + MycroftMessageListener mockListener = Mockito.mock(MycroftMessageListener.class); + ArgumentCaptor argCaptorMessage = ArgumentCaptor.forClass(MessageSpeak.class); + + // given we register speak listener + mycroftConnection.registerListener(MessageType.speak, mockListener); + + // when we send speak message + @SuppressWarnings("null") + String speakMessageJson = new String( + MycroftConnectionTest.class.getResourceAsStream("speak.json").readAllBytes(), "UTF-8"); + mycroftConnection.onMessage(sessionMock, speakMessageJson); + + // then message is correctly received by listener + Mockito.verify(mockListener).baseMessageReceived(argCaptorMessage.capture()); + + assertEquals(argCaptorMessage.getValue().data.utterance, "coucou"); + } +} diff --git a/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json b/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json new file mode 100644 index 0000000000000..dcb8697be4d86 --- /dev/null +++ b/bundles/org.openhab.binding.mycroft/src/test/resources/org/openhab/binding/mycroft/internal/api/speak.json @@ -0,0 +1 @@ +{"type": "speak", "data": {"utterance": "coucou", "expect_response": false, "meta": {"skill": "SpeakSkill"}, "is_error": false}, "context": {"client_name": "mycroft_cli", "source": ["skills"], "destination": "debug_cli"}} \ No newline at end of file diff --git a/bundles/pom.xml b/bundles/pom.xml index fa0385512abd7..8d9018054179d 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -220,6 +220,7 @@ org.openhab.binding.mqtt.generic org.openhab.binding.mqtt.homeassistant org.openhab.binding.mqtt.homie + org.openhab.binding.mycroft org.openhab.binding.myq org.openhab.binding.mystrom org.openhab.binding.nanoleaf