/**
* Copyright (c) 2010-2016 by the respective copyright holders.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.openhab.action.squeezebox.internal;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.openhab.core.scriptengine.action.ActionDoc;
import org.openhab.core.scriptengine.action.ParamDoc;
import org.openhab.io.squeezeserver.SqueezePlayer;
import org.openhab.io.squeezeserver.SqueezePlayer.Mode;
import org.openhab.io.squeezeserver.SqueezeServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class contains the methods that are made available in scripts and rules
* for Squeezebox integration.
*
* @author Ben Jones
* @since 1.4.0
*/
public class Squeezebox {
private static final Logger logger = LoggerFactory.getLogger(Squeezebox.class);
// handle to the Squeeze Server connection
public static SqueezeServer squeezeServer;
private static boolean isReady() {
if (squeezeServer == null) {
logger.debug("Squeezebox action is not yet configured - execution aborted!");
return false;
}
return true;
}
@ActionDoc(text = "Turn one of your Squeezebox devices on/off", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPower(
@ParamDoc(name = "playerId", text = "The Squeezebox to turn on/off") String playerId,
@ParamDoc(name = "power", text = "True to turn on, False to turn off") boolean power) {
if (!isReady()) {
return false;
}
if (power) {
return squeezeServer.powerOn(playerId);
} else {
return squeezeServer.powerOff(playerId);
}
}
@ActionDoc(text = "Mute/unmute one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxMute(
@ParamDoc(name = "playerId", text = "The Squeezebox to turn on/off") String playerId,
@ParamDoc(name = "mute", text = "True to mute, False to un-mute") boolean mute) {
if (!isReady()) {
return false;
}
if (mute) {
return squeezeServer.mute(playerId);
} else {
return squeezeServer.unMute(playerId);
}
}
@ActionDoc(text = "Set the volume on one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxVolume(
@ParamDoc(name = "playerId", text = "The Squeezebox to turn on/off") String playerId,
@ParamDoc(name = "volume", text = "The volume between 0-100") int volume) {
if (!isReady()) {
return false;
}
return squeezeServer.setVolume(playerId, volume);
}
@ActionDoc(text = "Send the 'play' command to one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPlay(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the command to") String playerId) {
if (!isReady()) {
return false;
}
return squeezeServer.play(playerId);
}
@ActionDoc(text = "Send the 'pause' command to one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPause(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the command to") String playerId) {
if (!isReady()) {
return false;
}
return squeezeServer.pause(playerId);
}
@ActionDoc(text = "Send the 'stop' command to one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxStop(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the command to") String playerId) {
if (!isReady()) {
return false;
}
return squeezeServer.stop(playerId);
}
@ActionDoc(text = "Send the 'next' command to one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxNext(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the command to") String playerId) {
if (!isReady()) {
return false;
}
return squeezeServer.next(playerId);
}
@ActionDoc(text = "Send the 'prev' command to one of your Squeezebox devices", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPrev(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the command to") String playerId) {
if (!isReady()) {
return false;
}
return squeezeServer.prev(playerId);
}
@ActionDoc(text = "Play a URL on one of your Squeezebox devices using the current volume for that device", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPlayUrl(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the URL to") String playerId,
@ParamDoc(name = "url", text = "The URL to play (if empty will clear the playlist)") String url) {
if (!isReady()) {
return false;
}
return squeezeboxPlayUrl(playerId, url, -1);
}
@ActionDoc(text = "Play a URL on one of your Squeezebox devices using the specified volume", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPlayUrl(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the URL to") String playerId,
@ParamDoc(name = "url", text = "The URL to play (if empty will clear the playlist)") String url,
@ParamDoc(name = "volume", text = "The volume to set the device when playing this URL (between 1-100)") int volume) {
if (!isReady()) {
return false;
}
// set the player ready to play this URL
if (volume != -1) {
logger.trace("Setting player state: volume {}", volume);
squeezeServer.setVolume(playerId, volume);
}
// play the url
if (StringUtils.isEmpty(url)) {
return squeezeServer.clearPlaylist(playerId);
} else {
return squeezeServer.playUrl(playerId, url);
}
}
@ActionDoc(text = "Issues an arbitrary command to a player", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxPlayerCommand(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the URL to") String playerId,
@ParamDoc(name = "command", text = "A command to send to the player") String command) {
if (!isReady()) {
return false;
}
return squeezeServer.playerCommand(playerId, command);
}
@ActionDoc(text = "Speak a message via one of your Squeezebox devices using the current volume for that device", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxSpeak(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the message to") String playerId,
@ParamDoc(name = "message", text = "The message to say") String message) {
if (!isReady()) {
return false;
}
return squeezeboxSpeak(playerId, message, -1, true);
}
@ActionDoc(text = "Speak a message via one of your Squeezebox devices using the specified volume and always resume previous playback", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxSpeak(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the message to") String playerId,
@ParamDoc(name = "message", text = "The message to say") String message,
@ParamDoc(name = "volume", text = "The volume to set the device when speaking this message (between 1-100)") int volume) {
if (!isReady()) {
return false;
}
return squeezeboxSpeak(playerId, message, volume, true);
}
@ActionDoc(text = "Speak a message via one of your Squeezebox devices using the specified volume and using specified resume status", returns = "<code>true</code>, if successful and <code>false</code> otherwise.")
public static boolean squeezeboxSpeak(
@ParamDoc(name = "playerId", text = "The Squeezebox to send the message to") String playerId,
@ParamDoc(name = "message", text = "The message to say") String message,
@ParamDoc(name = "volume", text = "The volume to set the device when speaking this message (between 1-100)") int volume,
@ParamDoc(name = "resumePlayback", text = "Continue playback after speech") Boolean resumePlayback) {
if (!isReady()) {
return false;
}
// get the player - can return null if the playerId is bogus
SqueezePlayer player = squeezeServer.getPlayer(playerId);
if (player == null) {
return false;
}
logger.trace("***START SPEECH**** Player: '{}'", playerId);
// get the current player state
int playerVolume = player.getUnmuteVolume();
boolean playerPowered = player.isPowered();
boolean playerMuted = player.isMuted();
Mode playerMode = player.getMode();
int currNumTracks = player.getNumberPlaylistTracks();
int currPlayingTime = player.getCurrentPlayingTime();
int currPlaylistIndex = player.getCurrentPlaylistIndex();
int currPlaylistShuffle = player.getCurrentPlaylistShuffle();
int currPlaylistRepeat = player.getCurrentPlaylistRepeat();
int newNumTracks = 0;
logger.trace("Current Playing Mode '{}'", playerMode.toString());
logger.trace("Current Volume '{}'", playerVolume);
logger.trace("Current Num Playlist Tracks '{}'", currNumTracks);
logger.trace("Current Playing Playlist Index '{}'", currPlaylistIndex);
logger.trace("Current Playing Time '{}'", currPlayingTime);
logger.trace("Current Shuffle Mode '{}'", currPlaylistShuffle);
logger.trace("Current Repeat Mode '{}'", currPlaylistRepeat);
// If Playing Pause player before adjusting volume!
if (playerMode == Mode.play) {
squeezeServer.pause(playerId);
}
// set the player ready to play this announcement
if (playerMuted) {
logger.trace("Setting player state: unmuted");
squeezeServer.unMute(playerId);
}
if (volume != -1) {
logger.trace("Setting player state: volume {}", volume);
squeezeServer.setVolume(playerId, volume);
}
if (currPlaylistRepeat != 0) {
squeezeServer.setRepeatMode(playerId, 0);
}
if (currPlaylistShuffle != 0) {
squeezeServer.setShuffleMode(playerId, 0);
currPlaylistIndex = 0;
logger.trace("Shuffle Changed! Set Current Playing Index to 0");
}
// can only 'say' 100 chars at a time
List<String> sentences = getSentences(message, squeezeServer.getTtsMaxSentenceLength());
// send each sentence in turn
for (String sentence : sentences) {
logger.trace("Sending sentence to " + playerId + " (" + sentence + ")");
String encodedSentence;
try {
encodedSentence = URLEncoder.encode(sentence, "UTF-8");
} catch (UnsupportedEncodingException e) {
logger.warn("Failed to encode sentence '" + sentence + "'. Skipping sentence.", e);
continue;
}
encodedSentence = encodedSentence.replace("+", "%20");
logger.trace("Encoded sentence " + encodedSentence);
// build the URL to send to the Squeezebox to play
String url = String.format(squeezeServer.getTtsUrl(), encodedSentence);
// create an instance of our special listener so we can detect when the sentence is complete
SqueezeboxSentenceListener listener = new SqueezeboxSentenceListener(playerId);
squeezeServer.addPlayerEventListener(listener);
// send the URL (this will power up the player and un-mute if necessary)
logger.trace("Adding URL to current playlist '{}' to play", url);
squeezeServer.addPlaylistItem(playerId, url);
logger.trace("Sleeping for 1s for updated playlist to refresh", url);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
continue;
}
newNumTracks = player.getNumberPlaylistTracks();
logger.trace("New Playlist Track Number: '{}'", newNumTracks);
squeezeServer.playPlaylistItem(playerId, newNumTracks - 1);
squeezeServer.play(playerId);
// wait for this message to complete (timing out after 30s)
int timeoutCount = 0;
while (!listener.isFinished() && timeoutCount < 300) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
break;
}
timeoutCount++;
}
if (timeoutCount >= 200) {
logger.warn("Sentence timed out while speaking!");
}
squeezeServer.stop(playerId);
// clean up the listener
squeezeServer.removePlayerEventListener(listener);
listener = null;
logger.trace("Done playing speech - restore state...");
}
logger.trace("Deleting Playlist Index: '{}'", newNumTracks - 1);
squeezeServer.deletePlaylistItem(playerId, newNumTracks - 1);
// restore the player volume before playback
if (volume != -1) {
logger.trace("Restoring player to previous state: volume {}", playerVolume);
squeezeServer.setVolume(playerId, playerVolume);
}
if (playerMode != Mode.stop) {
logger.trace("Restoring Playlist Index Number: '{}'", currPlaylistIndex);
squeezeServer.playPlaylistItem(playerId, currPlaylistIndex);
logger.trace("Restoring Playing Time : '{}'", currPlayingTime);
squeezeServer.setPlayingTime(playerId, currPlayingTime);
}
// Must sleep 350ms before restoring previous playback state...
try {
Thread.sleep(350);
} catch (InterruptedException e) {
}
// restore play mode state
if (playerMode == Mode.play) {
if (resumePlayback) {
logger.trace("Restoring Playing Mode: '{}'", playerMode);
squeezeServer.play(playerId);
} else {
logger.warn("NOT restoring Playing Mode: '{}' because resumePlayback is false", playerMode);
squeezeServer.pause(playerId);
}
} else if (playerMode == Mode.pause) {
squeezeServer.pause(playerId);
} else {
squeezeServer.stop(playerId);
}
logger.trace("Restoring player to previous state: shuffle {}", currPlaylistShuffle);
squeezeServer.setShuffleMode(playerId, currPlaylistShuffle);
logger.trace("Restoring player to previous state: repeat {}", currPlaylistRepeat);
squeezeServer.setRepeatMode(playerId, currPlaylistRepeat);
if (playerMuted) {
logger.trace("Restoring player to previous state: muted");
squeezeServer.mute(playerId);
}
if (!playerPowered) {
logger.trace("Restoring player to previous state: off");
squeezeServer.powerOff(playerId);
}
logger.trace("*****DONE SPEECH****** Player: '{}'", playerId);
return true;
}
private static List<String> getSentences(String message, int maxSentenceLength) {
List<String> sentences = new ArrayList<String>();
if (StringUtils.isEmpty(message)) {
return sentences;
}
if (message.length() <= maxSentenceLength) {
sentences.add(message.trim());
return sentences;
}
String current = "";
for (String sentence : StringUtils.split(message, '.')) {
sentence = sentence.trim();
if (sentence.length() == 0) {
continue;
}
// if this sentence is too long then split up
if (sentence.length() > maxSentenceLength) {
if (current.length() > 0) {
sentences.add(current.trim());
current = "";
}
// split this long sentence up and add each part
sentences.addAll(splitSentence(sentence, maxSentenceLength));
} else {
if (current.length() + sentence.length() + 2 > maxSentenceLength) {
sentences.add(current.trim());
current = "";
}
// add this sentence to the current phrase
current += sentence + ". ";
}
}
// add the final sentence
if (current.length() > 0) {
sentences.add(current.trim());
}
return sentences;
}
private static List<String> splitSentence(String sentence, int maxSentenceLength) {
List<String> parts = new ArrayList<String>();
if (StringUtils.isEmpty(sentence)) {
return parts;
}
if (sentence.length() <= maxSentenceLength) {
parts.add(sentence.trim());
return parts;
}
String current = "";
for (String word : StringUtils.split(sentence, ' ')) {
word = word.trim();
if (word.length() == 0) {
continue;
}
// check this word isn't too long by itself
if (word.length() > maxSentenceLength) {
logger.warn("Unable to say '{}' as this word is longer than the maximum sentence allowed ({})", word,
maxSentenceLength);
continue;
}
// if this word makes our sentence too long start a new sentence
if (current.length() + word.length() > maxSentenceLength) {
parts.add(current.trim());
current = "";
}
// add this word to the current sentence
current += word + " ";
}
// add the final sentence
if (current.length() > 0) {
parts.add(current.trim());
}
return parts;
}
}