diff --git a/bundles/org.openhab.binding.amazonechocontrol/README.md b/bundles/org.openhab.binding.amazonechocontrol/README.md index 7837ec06acf..a9f46ae0cf4 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/README.md +++ b/bundles/org.openhab.binding.amazonechocontrol/README.md @@ -5,6 +5,7 @@ This binding can control Amazon Echo devices (Alexa). It provides features to control and view the current state of echo devices: - use echo device as text to speech from a rule +- execute a text command - volume - pause/continue/next track/previous track - connect/disconnect bluetooth devices @@ -179,6 +180,7 @@ It will be configured at runtime by using the save channel to store the current | announcement | String | W | echo, echoshow, echospot | Write Only! Display the announcement message on the display. See in the tutorial section to learn how it’s possible to set the title and turn off the sound. | textToSpeech | String | W | echo, echoshow, echospot | Write Only! Write some text to this channel and Alexa will speak it. It is possible to use plain text or SSML: e.g. `I want to tell you a secret.I am not a real human.` | textToSpeechVolume | Dimmer | R/W | echo, echoshow, echospot | Volume of the textToSpeech channel, if 0 the current volume will be used +| textCommand | String | W | echo, echoshow, echospot | Write Only! Execute a text command (like a spoken text) | lastVoiceCommand | String | R/W | echo, echoshow, echospot | Last voice command spoken to the device. Writing to the channel starts voice output. | mediaProgress | Dimmer | R/W | echo, echoshow, echospot | Media progress in percent | mediaProgressTime | Number:Time | R/W | echo, echoshow, echospot | Media play time @@ -199,7 +201,6 @@ E.g. to read out the history call from an installation on openhab:8080 with an a http://openhab:8080/amazonechocontrol/account1/PROXY/api/activities?startTime=&size=50&offset=1 - ### Example #### echo.things diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java index 7f013bab7ba..29571574a5c 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/AmazonEchoControlBindingConstants.java @@ -76,6 +76,7 @@ public class AmazonEchoControlBindingConstants { public static final String CHANNEL_AMAZON_MUSIC_PLAY_LIST_ID = "amazonMusicPlayListId"; public static final String CHANNEL_TEXT_TO_SPEECH = "textToSpeech"; public static final String CHANNEL_TEXT_TO_SPEECH_VOLUME = "textToSpeechVolume"; + public static final String CHANNEL_TEXT_COMMAND = "textCommand"; public static final String CHANNEL_REMIND = "remind"; public static final String CHANNEL_PLAY_ALARM_SOUND = "playAlarmSound"; public static final String CHANNEL_START_ROUTINE = "startRoutine"; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java index 587a89fecb2..22aba40fb62 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/Connection.java @@ -162,6 +162,8 @@ public class Connection { private Map announcements = Collections.synchronizedMap(new LinkedHashMap<>()); private Map textToSpeeches = Collections.synchronizedMap(new LinkedHashMap<>()); + private Map textCommands = Collections.synchronizedMap(new LinkedHashMap<>()); + private Map volumes = Collections.synchronizedMap(new LinkedHashMap<>()); private Map> devices = Collections.synchronizedMap(new LinkedHashMap<>()); @@ -172,7 +174,8 @@ public class Connection { ANNOUNCEMENT, TTS, VOLUME, - DEVICES + DEVICES, + TEXT_COMMAND } public Connection(@Nullable Connection oldConnection, Gson gson) { @@ -962,6 +965,8 @@ public class Connection { replaceTimer(TimerType.VOLUME, null); volumes.clear(); replaceTimer(TimerType.DEVICES, null); + textCommands.clear(); + replaceTimer(TimerType.TTS, null); devices.values().forEach((queueObjects) -> { queueObjects.forEach((queueObject) -> { @@ -1354,7 +1359,7 @@ public class Connection { private void sendAnnouncement() { // we lock new announcements until we have dispatched everything - Lock lock = locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.ANNOUNCEMENT, k -> new ReentrantLock())); lock.lock(); try { Iterator iterator = announcements.values().iterator(); @@ -1396,7 +1401,7 @@ public class Connection { } // we lock TTS until we have finished adding this one - Lock lock = locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); lock.lock(); try { TextToSpeech textToSpeech = Objects @@ -1414,7 +1419,7 @@ public class Connection { private void sendTextToSpeech() { // we lock new TTS until we have dispatched everything - Lock lock = locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TTS, k -> new ReentrantLock())); lock.lock(); try { Iterator iterator = textToSpeeches.values().iterator(); @@ -1424,8 +1429,7 @@ public class Connection { List devices = textToSpeech.devices; if (!devices.isEmpty()) { String text = textToSpeech.text; - Map parameters = new HashMap<>(); - parameters.put("textToSpeak", text); + Map parameters = Map.of("textToSpeak", text); executeSequenceCommandWithVolume(devices, "Alexa.Speak", parameters, textToSpeech.ttsVolumes, textToSpeech.standardVolumes); } @@ -1441,9 +1445,60 @@ public class Connection { } } + public void textCommand(Device device, String text, @Nullable Integer ttsVolume, @Nullable Integer standardVolume) { + if (text.replaceAll("<.+?>", "").replaceAll("\\s+", " ").trim().isEmpty()) { + return; + } + + // we lock TextCommands until we have finished adding this one + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); + lock.lock(); + try { + TextCommand textCommand = Objects + .requireNonNull(textCommands.computeIfAbsent(Objects.hash(text), k -> new TextCommand(text))); + textCommand.devices.add(device); + textCommand.ttsVolumes.add(ttsVolume); + textCommand.standardVolumes.add(standardVolume); + // schedule a TextCommand only if it has not been scheduled before + timers.computeIfAbsent(TimerType.TEXT_COMMAND, + k -> scheduler.schedule(this::sendTextCommand, 500, TimeUnit.MILLISECONDS)); + } finally { + lock.unlock(); + } + } + + private synchronized void sendTextCommand() { + // we lock new TTS until we have dispatched everything + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.TEXT_COMMAND, k -> new ReentrantLock())); + lock.lock(); + + try { + Iterator iterator = textCommands.values().iterator(); + while (iterator.hasNext()) { + TextCommand textCommand = iterator.next(); + try { + List devices = textCommand.devices; + if (!devices.isEmpty()) { + String text = textCommand.text; + Map parameters = Map.of("text", text); + executeSequenceCommandWithVolume(devices, "Alexa.TextCommand", parameters, + textCommand.ttsVolumes, textCommand.standardVolumes); + } + } catch (Exception e) { + logger.warn("send textCommand fails with unexpected error", e); + } + iterator.remove(); + } + } finally { + // the timer is done anyway immediately after we unlock + timers.remove(TimerType.TEXT_COMMAND); + lock.unlock(); + } + } + public void volume(Device device, int vol) { // we lock volume until we have finished adding this one - Lock lock = locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); lock.lock(); try { Volume volume = Objects.requireNonNull(volumes.computeIfAbsent(vol, k -> new Volume(vol))); @@ -1459,7 +1514,7 @@ public class Connection { private void sendVolume() { // we lock new volume until we have dispatched everything - Lock lock = locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.VOLUME, k -> new ReentrantLock())); lock.lock(); try { Iterator iterator = volumes.values().iterator(); @@ -1504,7 +1559,7 @@ public class Connection { if (command != null && !parameters.isEmpty()) { JsonArray commandNodesToExecute = new JsonArray(); - if ("Alexa.Speak".equals(command)) { + if ("Alexa.Speak".equals(command) || "Alexa.TextCommand".equals(command)) { for (Device device : devices) { commandNodesToExecute.add(createExecutionNode(device, command, parameters)); } @@ -1565,7 +1620,7 @@ public class Connection { } private void handleExecuteSequenceNode() { - Lock lock = locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock()); + Lock lock = Objects.requireNonNull(locks.computeIfAbsent(TimerType.DEVICES, k -> new ReentrantLock())); if (lock.tryLock()) { try { for (String serialNumber : devices.keySet()) { @@ -1707,6 +1762,9 @@ public class Connection { JsonObject nodeToExecute = new JsonObject(); nodeToExecute.addProperty("@type", "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode"); nodeToExecute.addProperty("type", command); + if ("Alexa.TextCommand".equals(command)) { + nodeToExecute.addProperty("skillId", "amzn1.ask.1p.tellalexa"); + } nodeToExecute.add("operationPayload", operationPayload); return nodeToExecute; } @@ -2047,6 +2105,17 @@ public class Connection { } } + private static class TextCommand { + public List devices = new ArrayList<>(); + public String text; + public List<@Nullable Integer> ttsVolumes = new ArrayList<>(); + public List<@Nullable Integer> standardVolumes = new ArrayList<>(); + + public TextCommand(String text) { + this.text = text; + } + } + private static class Volume { public List devices = new ArrayList<>(); public int volume; diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java index 20a45423caf..fa774b91138 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/AccountHandler.java @@ -743,11 +743,6 @@ public class AccountHandler extends BaseBridgeHandler implements IWebSocketComma switch (command) { case "PUSH_ACTIVITY": handlePushActivity(pushCommand.payload); - if (refreshDataDelayed != null) { - refreshDataDelayed.cancel(false); - } - this.refreshAfterCommandJob = scheduler.schedule(this::refreshAfterCommand, 700, - TimeUnit.MILLISECONDS); break; case "PUSH_DOPPLER_CONNECTION_CHANGE": case "PUSH_BLUETOOTH_STATE_CHANGE": diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java index 5e4d26ead43..3fc7fec488a 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/java/org/openhab/binding/amazonechocontrol/internal/handler/EchoHandler.java @@ -114,6 +114,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { private boolean disableUpdate = false; private boolean updateRemind = true; private boolean updateTextToSpeech = true; + private boolean updateTextCommand = true; private boolean updateAlarm = true; private boolean updateRoutine = true; private boolean updatePlayMusicVoiceCommand = true; @@ -589,6 +590,16 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { } this.updateState(channelId, new PercentType(textToSpeechVolume)); } + if (channelId.equals(CHANNEL_TEXT_COMMAND)) { + if (command instanceof StringType) { + String text = command.toFullString(); + if (!text.isEmpty()) { + waitForUpdate = 1000; + updateTextCommand = true; + startTextCommand(connection, device, text); + } + } + } if (channelId.equals(CHANNEL_LAST_VOICE_COMMAND)) { if (command instanceof StringType) { String text = command.toFullString(); @@ -713,6 +724,15 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { connection.textToSpeech(device, text, volume, lastKnownVolume); } + private void startTextCommand(Connection connection, Device device, String text) + throws IOException, URISyntaxException { + Integer volume = null; + if (textToSpeechVolume != 0) { + volume = textToSpeechVolume; + } + connection.textCommand(device, text, volume, lastKnownVolume); + } + @Override public void startAnnouncement(Device device, String speak, String bodyText, @Nullable String title, @Nullable Integer volume) throws IOException, URISyntaxException { @@ -770,11 +790,11 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { String type = currentNotification.type; if (type != null) { if (type.equals("Reminder")) { - updateState(CHANNEL_REMIND, new StringType("")); + updateState(CHANNEL_REMIND, StringType.EMPTY); updateRemind = false; } if (type.equals("Alarm")) { - updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); + updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY); updateAlarm = false; } } @@ -919,7 +939,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { } } catch (HttpException e) { if (e.getCode() == 400) { - updateState(CHANNEL_RADIO_STATION_ID, new StringType("")); + updateState(CHANNEL_RADIO_STATION_ID, StringType.EMPTY); } else { logger.info("getMediaState fails", e); } @@ -1069,27 +1089,31 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { // Update states if (updateRemind && currentNotifcationUpdateTimer == null) { updateRemind = false; - updateState(CHANNEL_REMIND, new StringType("")); + updateState(CHANNEL_REMIND, StringType.EMPTY); } if (updateAlarm && currentNotifcationUpdateTimer == null) { updateAlarm = false; - updateState(CHANNEL_PLAY_ALARM_SOUND, new StringType("")); + updateState(CHANNEL_PLAY_ALARM_SOUND, StringType.EMPTY); } if (updateRoutine) { updateRoutine = false; - updateState(CHANNEL_START_ROUTINE, new StringType("")); + updateState(CHANNEL_START_ROUTINE, StringType.EMPTY); } if (updateTextToSpeech) { updateTextToSpeech = false; - updateState(CHANNEL_TEXT_TO_SPEECH, new StringType("")); + updateState(CHANNEL_TEXT_TO_SPEECH, StringType.EMPTY); + } + if (updateTextCommand) { + updateTextCommand = false; + updateState(CHANNEL_TEXT_COMMAND, StringType.EMPTY); } if (updatePlayMusicVoiceCommand) { updatePlayMusicVoiceCommand = false; - updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, new StringType("")); + updateState(CHANNEL_PLAY_MUSIC_VOICE_COMMAND, StringType.EMPTY); } if (updateStartCommand) { updateStartCommand = false; - updateState(CHANNEL_START_COMMAND, new StringType("")); + updateState(CHANNEL_START_COMMAND, StringType.EMPTY); } updateState(CHANNEL_MUSIC_PROVIDER_ID, new StringType(musicProviderId)); @@ -1225,7 +1249,7 @@ public class EchoHandler extends BaseThingHandler implements IEchoThingHandler { } if (lastSpokenText.isEmpty() || lastSpokenText.equals(spokenText)) { - updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType("")); + updateState(CHANNEL_LAST_VOICE_COMMAND, StringType.EMPTY); } lastSpokenText = spokenText; updateState(CHANNEL_LAST_VOICE_COMMAND, new StringType(spokenText)); diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol_de.properties b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol_de.properties index 1661f411aa4..ebb0fc87d2f 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol_de.properties +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/i18n/amazonechocontrol_de.properties @@ -88,6 +88,9 @@ channel-type.amazonechocontrol.textToSpeech.description = Spricht den Text (Nur channel-type.amazonechocontrol.textToSpeechVolume.label = Sprich Lautstärke channel-type.amazonechocontrol.textToSpeechVolume.description = Lautstärke des Sprich Kanals. Wenn 0 wird die aktuelle Lautstärke verwendet. +channel-type.amazonechocontrol.textCommand.label = Befehl +channel-type.amazonechocontrol.textCommand.description = Führt einen Befehl aus (Nur schreiben). Der Befehl wird wie ein gesprochener Befehl ausgeführt. + channel-type.amazonechocontrol.lastVoiceCommand.label = Letzter Sprachbefehl channel-type.amazonechocontrol.lastVoiceCommand.description = Befehl der zum Gerät gesprochen wurde. Schreiben zum Kanal started die Sprachausgabe. diff --git a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml index 13f81cf1c51..421a502548e 100644 --- a/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.amazonechocontrol/src/main/resources/OH-INF/thing/thing-types.xml @@ -73,6 +73,7 @@ + @@ -127,6 +128,7 @@ + @@ -181,6 +183,7 @@ + @@ -452,6 +455,11 @@ Volume of the Speak channel. If 0, the current volume will be used. + + String + + Run a command (Write only). The command can run like a spoken command. + String