added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.squeezebox-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-squeezebox" description="Squeezebox Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-upnp</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.squeezebox/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
|
||||
import org.openhab.core.audio.AudioFormat;
|
||||
import org.openhab.core.audio.AudioHTTPServer;
|
||||
import org.openhab.core.audio.AudioSink;
|
||||
import org.openhab.core.audio.AudioStream;
|
||||
import org.openhab.core.audio.FileAudioStream;
|
||||
import org.openhab.core.audio.FixedLengthAudioStream;
|
||||
import org.openhab.core.audio.URLAudioStream;
|
||||
import org.openhab.core.audio.UnsupportedAudioFormatException;
|
||||
import org.openhab.core.audio.UnsupportedAudioStreamException;
|
||||
import org.openhab.core.audio.utils.AudioStreamUtils;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This makes a SqueezeBox Player serve as an {@link AudioSink}-
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
* @author Mark Hilbush - Add callbackUrl
|
||||
*/
|
||||
public class SqueezeBoxAudioSink implements AudioSink {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxAudioSink.class);
|
||||
|
||||
private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
|
||||
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
|
||||
|
||||
// Needed because Squeezebox does multiple requests for the stream
|
||||
private static final int STREAM_TIMEOUT = 15;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
static {
|
||||
SUPPORTED_FORMATS.add(AudioFormat.WAV);
|
||||
SUPPORTED_FORMATS.add(AudioFormat.MP3);
|
||||
|
||||
SUPPORTED_STREAMS.add(FixedLengthAudioStream.class);
|
||||
SUPPORTED_STREAMS.add(URLAudioStream.class);
|
||||
}
|
||||
|
||||
private AudioHTTPServer audioHTTPServer;
|
||||
private SqueezeBoxPlayerHandler playerHandler;
|
||||
|
||||
public SqueezeBoxAudioSink(SqueezeBoxPlayerHandler playerHandler, AudioHTTPServer audioHTTPServer,
|
||||
String callbackUrl) {
|
||||
this.playerHandler = playerHandler;
|
||||
this.audioHTTPServer = audioHTTPServer;
|
||||
this.callbackUrl = callbackUrl;
|
||||
if (StringUtils.isNotEmpty(callbackUrl)) {
|
||||
logger.debug("SqueezeBox AudioSink created with callback URL {}", callbackUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return playerHandler.getThing().getUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel(Locale locale) {
|
||||
return playerHandler.getThing().getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(AudioStream audioStream)
|
||||
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
|
||||
AudioFormat format = audioStream.getFormat();
|
||||
if (!AudioFormat.WAV.isCompatible(format) && !AudioFormat.MP3.isCompatible(format)) {
|
||||
throw new UnsupportedAudioFormatException("Currently only MP3 and WAV formats are supported: ", format);
|
||||
}
|
||||
|
||||
String url;
|
||||
if (audioStream instanceof URLAudioStream) {
|
||||
url = ((URLAudioStream) audioStream).getURL();
|
||||
} else if (audioStream instanceof FixedLengthAudioStream) {
|
||||
// Since Squeezebox will make multiple requests for the stream, set a timeout on the stream
|
||||
url = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, STREAM_TIMEOUT).toString();
|
||||
|
||||
if (AudioFormat.WAV.isCompatible(format)) {
|
||||
url += AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.WAV_EXTENSION;
|
||||
} else if (AudioFormat.MP3.isCompatible(format)) {
|
||||
url += AudioStreamUtils.EXTENSION_SEPARATOR + FileAudioStream.MP3_EXTENSION;
|
||||
}
|
||||
|
||||
// Form the URL for streaming the notification from the OH2 web server
|
||||
// Use the callback URL if it is set in the binding configuration
|
||||
String host = StringUtils.isEmpty(callbackUrl) ? playerHandler.getHostAndPort() : callbackUrl;
|
||||
if (host == null) {
|
||||
logger.warn("Unable to get host/port from which to stream notification");
|
||||
return;
|
||||
}
|
||||
url = host + url;
|
||||
} else {
|
||||
throw new UnsupportedAudioStreamException(
|
||||
"SqueezeBox can only handle URLAudioStream or FixedLengthAudioStreams.", null);
|
||||
}
|
||||
|
||||
logger.debug("Processing audioStream {} of format {}", url, format);
|
||||
playerHandler.playNotificationSoundURI(new StringType(url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AudioFormat> getSupportedFormats() {
|
||||
return SUPPORTED_FORMATS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends AudioStream>> getSupportedStreams() {
|
||||
return SUPPORTED_STREAMS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PercentType getVolume() {
|
||||
return playerHandler.getNotificationSoundVolume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(PercentType volume) {
|
||||
playerHandler.setNotificationSoundVolume(volume);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link SqueezeBoxBinding} class defines common constants, which are used
|
||||
* across the whole binding.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Added duration channel
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SqueezeBoxBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "squeezebox";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID SQUEEZEBOXPLAYER_THING_TYPE = new ThingTypeUID(BINDING_ID, "squeezeboxplayer");
|
||||
public static final ThingTypeUID SQUEEZEBOXSERVER_THING_TYPE = new ThingTypeUID(BINDING_ID, "squeezeboxserver");
|
||||
|
||||
// List of all Server Channel Ids
|
||||
public static final String CHANNEL_FAVORITES_LIST = "favoritesList";
|
||||
|
||||
// List of all Player Channel Ids
|
||||
public static final String CHANNEL_POWER = "power";
|
||||
public static final String CHANNEL_MUTE = "mute";
|
||||
public static final String CHANNEL_VOLUME = "volume";
|
||||
public static final String CHANNEL_STOP = "stop";
|
||||
public static final String CHANNEL_PLAY_PAUSE = "playPause";
|
||||
public static final String CHANNEL_NEXT = "next";
|
||||
public static final String CHANNEL_PREV = "prev";
|
||||
public static final String CHANNEL_CONTROL = "control";
|
||||
public static final String CHANNEL_STREAM = "stream";
|
||||
public static final String CHANNEL_SOURCE = "source";
|
||||
public static final String CHANNEL_SYNC = "sync";
|
||||
public static final String CHANNEL_UNSYNC = "unsync";
|
||||
public static final String CHANNEL_PLAYLIST_INDEX = "playListIndex";
|
||||
public static final String CHANNEL_CURRENT_PLAYING_TIME = "currentPlayingTime";
|
||||
public static final String CHANNEL_DURATION = "duration";
|
||||
public static final String CHANNEL_NUMBER_PLAYLIST_TRACKS = "numberPlaylistTracks";
|
||||
public static final String CHANNEL_CURRENT_PLAYLIST_SHUFFLE = "currentPlaylistShuffle";
|
||||
public static final String CHANNEL_CURRENT_PLAYLIST_REPEAT = "currentPlaylistRepeat";
|
||||
public static final String CHANNEL_TITLE = "title";
|
||||
public static final String CHANNEL_REMOTE_TITLE = "remotetitle";
|
||||
public static final String CHANNEL_ALBUM = "album";
|
||||
public static final String CHANNEL_ARTIST = "artist";
|
||||
public static final String CHANNEL_YEAR = "year";
|
||||
public static final String CHANNEL_GENRE = "genre";
|
||||
public static final String CHANNEL_COVERART_DATA = "coverartdata";
|
||||
public static final String CHANNEL_IRCODE = "ircode";
|
||||
public static final String CHANNEL_IP = "ip";
|
||||
public static final String CHANNEL_UID = "uid";
|
||||
public static final String CHANNEL_TYPEID = "typeId";
|
||||
public static final String CHANNEL_NAME = "name";
|
||||
public static final String CHANNEL_MODEL = "model";
|
||||
public static final String CHANNEL_FAVORITES_PLAY = "playFavorite";
|
||||
public static final String CHANNEL_RATE = "rate";
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal;
|
||||
|
||||
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;
|
||||
|
||||
import java.util.Dictionary;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.openhab.binding.squeezebox.internal.discovery.SqueezeBoxPlayerDiscoveryParticipant;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerEventListener;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler;
|
||||
import org.openhab.core.audio.AudioHTTPServer;
|
||||
import org.openhab.core.audio.AudioSink;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.net.HttpServiceUtil;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.framework.ServiceRegistration;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SqueezeBoxHandlerFactory} is responsible for creating things and
|
||||
* thing handlers.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Cancel request player job when handler removed
|
||||
* @author Mark Hilbush - Add callbackUrl
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.squeezebox")
|
||||
public class SqueezeBoxHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxHandlerFactory.class);
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
|
||||
.concat(SqueezeBoxServerHandler.SUPPORTED_THING_TYPES_UIDS.stream(),
|
||||
SqueezeBoxPlayerHandler.SUPPORTED_THING_TYPES_UIDS.stream())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
private Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
|
||||
|
||||
private final AudioHTTPServer audioHTTPServer;
|
||||
private final NetworkAddressService networkAddressService;
|
||||
private final SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;
|
||||
|
||||
private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
|
||||
|
||||
// Callback url (scheme+server+port) to use for playing notification sounds
|
||||
private String callbackUrl = null;
|
||||
|
||||
@Activate
|
||||
public SqueezeBoxHandlerFactory(@Reference AudioHTTPServer audioHTTPServer,
|
||||
@Reference NetworkAddressService networkAddressService,
|
||||
@Reference SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) {
|
||||
this.audioHTTPServer = audioHTTPServer;
|
||||
this.networkAddressService = networkAddressService;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
Dictionary<String, Object> properties = componentContext.getProperties();
|
||||
callbackUrl = (String) properties.get("callbackUrl");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(SQUEEZEBOXSERVER_THING_TYPE)) {
|
||||
logger.trace("creating handler for bridge thing {}", thing);
|
||||
SqueezeBoxServerHandler bridge = new SqueezeBoxServerHandler((Bridge) thing);
|
||||
registerSqueezeBoxPlayerDiscoveryService(bridge);
|
||||
return bridge;
|
||||
}
|
||||
|
||||
if (thingTypeUID.equals(SQUEEZEBOXPLAYER_THING_TYPE)) {
|
||||
logger.trace("creating handler for player thing {}", thing);
|
||||
SqueezeBoxPlayerHandler playerHandler = new SqueezeBoxPlayerHandler(thing, createCallbackUrl(),
|
||||
stateDescriptionProvider);
|
||||
|
||||
// Register the player as an audio sink
|
||||
logger.trace("Registering an audio sink for player thing {}", thing.getUID());
|
||||
SqueezeBoxAudioSink audioSink = new SqueezeBoxAudioSink(playerHandler, audioHTTPServer, callbackUrl);
|
||||
@SuppressWarnings("unchecked")
|
||||
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
|
||||
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
|
||||
audioSinkRegistrations.put(thing.getUID().toString(), reg);
|
||||
|
||||
return playerHandler;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds SqueezeBoxServerHandlers to the discovery service to find SqueezeBox
|
||||
* Players
|
||||
*
|
||||
* @param squeezeBoxServerHandler
|
||||
*/
|
||||
private synchronized void registerSqueezeBoxPlayerDiscoveryService(
|
||||
SqueezeBoxServerHandler squeezeBoxServerHandler) {
|
||||
logger.trace("registering player discovery service");
|
||||
|
||||
SqueezeBoxPlayerDiscoveryParticipant discoveryService = new SqueezeBoxPlayerDiscoveryParticipant(
|
||||
squeezeBoxServerHandler);
|
||||
|
||||
// Register the PlayerListener with the SqueezeBoxServerHandler
|
||||
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(discoveryService);
|
||||
|
||||
// Register the service, then add the service to the ServiceRegistration map
|
||||
discoveryServiceRegs.put(squeezeBoxServerHandler.getThing().getUID(),
|
||||
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void removeHandler(ThingHandler thingHandler) {
|
||||
if (thingHandler instanceof SqueezeBoxServerHandler) {
|
||||
logger.trace("removing handler for bridge thing {}", thingHandler.getThing());
|
||||
|
||||
ServiceRegistration<?> serviceReg = this.discoveryServiceRegs.get(thingHandler.getThing().getUID());
|
||||
if (serviceReg != null) {
|
||||
logger.trace("unregistering player discovery service");
|
||||
|
||||
// Get the discovery service object and use it to cancel the RequestPlayerJob
|
||||
SqueezeBoxPlayerDiscoveryParticipant discoveryService = (SqueezeBoxPlayerDiscoveryParticipant) bundleContext
|
||||
.getService(serviceReg.getReference());
|
||||
discoveryService.cancelRequestPlayerJob();
|
||||
|
||||
// Unregister the PlayerListener from the SqueezeBoxServerHandler
|
||||
((SqueezeBoxServerHandler) thingHandler).unregisterSqueezeBoxPlayerListener(
|
||||
(SqueezeBoxPlayerEventListener) bundleContext.getService(serviceReg.getReference()));
|
||||
|
||||
// Unregister the PlayerListener service
|
||||
serviceReg.unregister();
|
||||
|
||||
// Remove the service from the ServiceRegistration map
|
||||
discoveryServiceRegs.remove(thingHandler.getThing().getUID());
|
||||
}
|
||||
}
|
||||
|
||||
if (thingHandler instanceof SqueezeBoxPlayerHandler) {
|
||||
SqueezeBoxServerHandler bridge = ((SqueezeBoxPlayerHandler) thingHandler).getSqueezeBoxServerHandler();
|
||||
if (bridge != null) {
|
||||
// Unregister the player's audio sink
|
||||
logger.trace("Unregistering the audio sync service for player thing {}",
|
||||
thingHandler.getThing().getUID());
|
||||
ServiceRegistration<AudioSink> reg = audioSinkRegistrations
|
||||
.get(thingHandler.getThing().getUID().toString());
|
||||
if (reg != null) {
|
||||
reg.unregister();
|
||||
}
|
||||
|
||||
logger.trace("removing handler for player thing {}", thingHandler.getThing());
|
||||
bridge.removePlayerCache(((SqueezeBoxPlayerHandler) thingHandler).getMac());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String createCallbackUrl() {
|
||||
final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
|
||||
if (ipAddress == null) {
|
||||
logger.warn("No network interface could be found.");
|
||||
return null;
|
||||
}
|
||||
|
||||
final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
|
||||
if (port == -1) {
|
||||
logger.warn("Cannot find port of the http service.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return "http://" + ipAddress + ":" + port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
|
||||
/**
|
||||
* Dynamic provider of state options while leaving other state description fields as original.
|
||||
*
|
||||
* @author Gregory Moyer - Initial contribution
|
||||
* @author Mark Hilbush - Adapted to squeezebox binding
|
||||
*/
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, SqueezeBoxStateDescriptionOptionsProvider.class })
|
||||
@NonNullByDefault
|
||||
public class SqueezeBoxStateDescriptionOptionsProvider extends BaseDynamicStateDescriptionProvider {
|
||||
|
||||
@Activate
|
||||
public SqueezeBoxStateDescriptionOptionsProvider(
|
||||
@Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
|
||||
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.config;
|
||||
|
||||
/**
|
||||
* Configuration for a player
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Convert sound notification volume from channel to config parameter
|
||||
*
|
||||
*/
|
||||
public class SqueezeBoxPlayerConfig {
|
||||
/**
|
||||
* MAC address of player
|
||||
*/
|
||||
public String mac;
|
||||
|
||||
/**
|
||||
* Number of seconds to wait to time out a notification
|
||||
*/
|
||||
public int notificationTimeout;
|
||||
|
||||
/**
|
||||
* Volume used for playing notifications
|
||||
*/
|
||||
public Integer notificationVolume;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.config;
|
||||
|
||||
/**
|
||||
* Configuration of a server.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Added user ID and password
|
||||
*
|
||||
*/
|
||||
public class SqueezeBoxServerConfig {
|
||||
/**
|
||||
* Server ip address
|
||||
*/
|
||||
public String ipAddress;
|
||||
/**
|
||||
* Server web port for REST calls
|
||||
*/
|
||||
public int webport;
|
||||
/**
|
||||
* Server cli port
|
||||
*/
|
||||
public int cliport;
|
||||
/**
|
||||
* Language for TTS
|
||||
*/
|
||||
public String language;
|
||||
/*
|
||||
* User ID (when authentication enabled in LMS)
|
||||
*/
|
||||
public String userId;
|
||||
/*
|
||||
* User ID (when authentication enabled in LMS)
|
||||
*/
|
||||
public String password;
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.SQUEEZEBOXPLAYER_THING_TYPE;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayer;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerEventListener;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler;
|
||||
import org.openhab.binding.squeezebox.internal.handler.SqueezeBoxServerHandler;
|
||||
import org.openhab.binding.squeezebox.internal.model.Favorite;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* When a {@link SqueezeBoxServerHandler} finds a new SqueezeBox Player we will
|
||||
* add it to the system.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - added method to cancel request player job, and to set thing properties
|
||||
* @author Mark Hilbush - Added duration channel
|
||||
* @author Mark Hilbush - Added event to update favorites list
|
||||
*
|
||||
*/
|
||||
public class SqueezeBoxPlayerDiscoveryParticipant extends AbstractDiscoveryService
|
||||
implements SqueezeBoxPlayerEventListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerDiscoveryParticipant.class);
|
||||
|
||||
private static final int TIMEOUT = 60;
|
||||
private static final int TTL = 60;
|
||||
|
||||
private SqueezeBoxServerHandler squeezeBoxServerHandler;
|
||||
private ScheduledFuture<?> requestPlayerJob;
|
||||
|
||||
/**
|
||||
* Discovers SqueezeBox Players attached to a SqueezeBox Server
|
||||
*
|
||||
* @param squeezeBoxServerHandler
|
||||
*/
|
||||
public SqueezeBoxPlayerDiscoveryParticipant(SqueezeBoxServerHandler squeezeBoxServerHandler) {
|
||||
super(SqueezeBoxPlayerHandler.SUPPORTED_THING_TYPES_UIDS, TIMEOUT, true);
|
||||
this.squeezeBoxServerHandler = squeezeBoxServerHandler;
|
||||
setupRequestPlayerJob();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("startScan invoked in SqueezeBoxPlayerDiscoveryParticipant");
|
||||
this.squeezeBoxServerHandler.requestPlayers();
|
||||
this.squeezeBoxServerHandler.requestFavorites();
|
||||
}
|
||||
|
||||
/*
|
||||
* Allows request player job to be canceled when server handler is removed
|
||||
*/
|
||||
public void cancelRequestPlayerJob() {
|
||||
logger.debug("canceling RequestPlayerJob");
|
||||
if (requestPlayerJob != null) {
|
||||
requestPlayerJob.cancel(true);
|
||||
requestPlayerJob = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playerAdded(SqueezeBoxPlayer player) {
|
||||
ThingUID bridgeUID = squeezeBoxServerHandler.getThing().getUID();
|
||||
|
||||
ThingUID thingUID = new ThingUID(SQUEEZEBOXPLAYER_THING_TYPE, bridgeUID,
|
||||
player.getMacAddress().replace(":", ""));
|
||||
|
||||
if (!playerThingExists(thingUID)) {
|
||||
logger.debug("player added {} : {} ", player.getMacAddress(), player.getName());
|
||||
|
||||
Map<String, Object> properties = new HashMap<>(1);
|
||||
String representationPropertyName = "mac";
|
||||
properties.put(representationPropertyName, player.getMacAddress());
|
||||
|
||||
// Added other properties
|
||||
properties.put("modelId", player.getModel());
|
||||
properties.put("name", player.getName());
|
||||
properties.put("uid", player.getUuid());
|
||||
properties.put("ip", player.getIpAddr());
|
||||
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
|
||||
.withRepresentationProperty(representationPropertyName).withBridge(bridgeUID)
|
||||
.withLabel(player.getName()).build();
|
||||
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean playerThingExists(ThingUID newThingUID) {
|
||||
return squeezeBoxServerHandler.getThing().getThing(newThingUID) != null ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the bridge to request a list of players
|
||||
*/
|
||||
private void setupRequestPlayerJob() {
|
||||
logger.debug("Request player job scheduled to run every {} seconds", TTL);
|
||||
requestPlayerJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
squeezeBoxServerHandler.requestPlayers();
|
||||
}, 10, TTL, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
// we can ignore the other events
|
||||
@Override
|
||||
public void powerChangeEvent(String mac, boolean power) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modeChangeEvent(String mac, String mode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void absoluteVolumeChangeEvent(String mac, int volume) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void muteChangeEvent(String mac, boolean mute) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistIndexEvent(String mac, int index) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlayingTimeEvent(String mac, int time) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void durationEvent(String mac, int duration) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void numberPlaylistTracksEvent(String mac, int track) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistRepeatEvent(String mac, int repeat) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void titleChangeEvent(String mac, String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void albumChangeEvent(String mac, String album) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void artistChangeEvent(String mac, String artist) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void coverArtChangeEvent(String mac, String coverArtUrl) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void yearChangeEvent(String mac, String year) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void genreChangeEvent(String mac, String genre) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteTitleChangeEvent(String mac, String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void irCodeChangeEvent(String mac, String ircode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFavoritesListEvent(List<Favorite> favorites) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sourceChangeEvent(String mac, String source) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.SQUEEZEBOXSERVER_THING_TYPE;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jupnp.model.meta.RemoteDevice;
|
||||
import org.openhab.binding.squeezebox.internal.utils.HttpUtils;
|
||||
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxCommunicationException;
|
||||
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxNotAuthorizedException;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.upnp.UpnpDiscoveryParticipant;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Discovers a SqueezeServer on the network using UPNP
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Add support for LMS authentication
|
||||
*
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
public class SqueezeBoxServerDiscoveryParticipant implements UpnpDiscoveryParticipant {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxServerDiscoveryParticipant.class);
|
||||
|
||||
/**
|
||||
* Name of a Squeeze Server
|
||||
*/
|
||||
private static final String MODEL_NAME = "Logitech Media Server";
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return Collections.singleton(SQUEEZEBOXSERVER_THING_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DiscoveryResult createResult(RemoteDevice device) {
|
||||
ThingUID uid = getThingUID(device);
|
||||
if (uid != null) {
|
||||
Map<String, Object> properties = new HashMap<>(3);
|
||||
|
||||
URI uri = device.getDetails().getPresentationURI();
|
||||
|
||||
String host = uri.getHost();
|
||||
int webPort = uri.getPort();
|
||||
int cliPort = 0;
|
||||
int defaultCliPort = 9090;
|
||||
|
||||
try {
|
||||
cliPort = HttpUtils.getCliPort(host, webPort);
|
||||
} catch (SqueezeBoxNotAuthorizedException e) {
|
||||
logger.debug("Not authorized to query CLI port. Using default of {}", defaultCliPort);
|
||||
cliPort = defaultCliPort;
|
||||
} catch (NumberFormatException e) {
|
||||
logger.debug("Badly formed CLI port. Using default of {}", defaultCliPort);
|
||||
cliPort = defaultCliPort;
|
||||
} catch (SqueezeBoxCommunicationException e) {
|
||||
logger.debug("Could not get cli port: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
|
||||
String label = device.getDetails().getFriendlyName();
|
||||
|
||||
String representationPropertyName = "ipAddress";
|
||||
properties.put(representationPropertyName, host);
|
||||
properties.put("webport", new Integer(webPort));
|
||||
properties.put("cliPort", new Integer(cliPort));
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withProperties(properties)
|
||||
.withRepresentationProperty(representationPropertyName).withLabel(label).build();
|
||||
|
||||
logger.debug("Created a DiscoveryResult for device '{}' with UDN '{}'",
|
||||
device.getDetails().getFriendlyName(), device.getIdentity().getUdn().getIdentifierString());
|
||||
return result;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThingUID getThingUID(RemoteDevice device) {
|
||||
if (device.getDetails().getFriendlyName() != null) {
|
||||
if (device.getDetails().getModelDetails().getModelName().contains(MODEL_NAME)) {
|
||||
logger.debug("Discovered a {} thing with UDN '{}'", device.getDetails().getFriendlyName(),
|
||||
device.getIdentity().getUdn().getIdentifierString());
|
||||
return new ThingUID(SQUEEZEBOXSERVER_THING_TYPE,
|
||||
device.getIdentity().getUdn().getIdentifierString().toUpperCase());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link ButtonDTO} represents a custom button that overrides existing
|
||||
* button functionality. For example, "like song" replaces the repeat button.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class ButtonDTO {
|
||||
|
||||
/**
|
||||
* Indicates whether button is standard or custom
|
||||
*/
|
||||
public Boolean custom;
|
||||
|
||||
/**
|
||||
* Indicates if standard button is enabled or disabled
|
||||
*/
|
||||
public Boolean enabled;
|
||||
|
||||
/**
|
||||
* Concatenation of elements of command array
|
||||
*/
|
||||
public String command;
|
||||
|
||||
/**
|
||||
* Currently not used
|
||||
*/
|
||||
@SerializedName("icon")
|
||||
public String icon;
|
||||
|
||||
/**
|
||||
* Currently not used
|
||||
*/
|
||||
@SerializedName("jiveStyle")
|
||||
public String jiveStyle;
|
||||
|
||||
/**
|
||||
* Currently not used
|
||||
*/
|
||||
@SerializedName("tooltip")
|
||||
public String toolTip;
|
||||
|
||||
public boolean isCustom() {
|
||||
return custom == null ? Boolean.FALSE : custom;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled == null ? Boolean.FALSE : enabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
/**
|
||||
* The {@link ButtonDTODeserializer} is responsible for deserializing a button object, which
|
||||
* can either be an Integer, or a custom button specification.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class ButtonDTODeserializer implements JsonDeserializer<ButtonDTO> {
|
||||
|
||||
@Override
|
||||
public ButtonDTO deserialize(JsonElement jsonElement, Type tyoeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
ButtonDTO button = null;
|
||||
if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isNumber()) {
|
||||
Integer value = jsonElement.getAsInt();
|
||||
button = new ButtonDTO();
|
||||
button.custom = false;
|
||||
button.enabled = value != 0;
|
||||
} else if (jsonElement.isJsonObject()) {
|
||||
JsonObject jsonObject = jsonElement.getAsJsonObject();
|
||||
button = new ButtonDTO();
|
||||
button.custom = true;
|
||||
button.icon = jsonObject.get("icon").getAsString();
|
||||
button.jiveStyle = jsonObject.get("jiveStyle").getAsString();
|
||||
button.toolTip = jsonObject.get("tooltip").getAsString();
|
||||
button.command = StreamSupport.stream(jsonObject.getAsJsonArray("command").spliterator(), false)
|
||||
.map(JsonElement::getAsString).collect(Collectors.joining(" "));
|
||||
}
|
||||
return button;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link ButtonsDTO} contains information about the forward, rewind, repeat,
|
||||
* and shuffle buttons, including any custom definitions, such as replacing repeat
|
||||
* and shuffle with like and unlike, respectively.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class ButtonsDTO {
|
||||
|
||||
/**
|
||||
* Indicates if forward button is enabled/disabled,
|
||||
* or if there is a custom button definition.
|
||||
*/
|
||||
@SerializedName("fwd")
|
||||
public ButtonDTO forward;
|
||||
|
||||
/**
|
||||
* Indicates if rewind button is enabled/disabled,
|
||||
* or if there is a custom button definition.
|
||||
*/
|
||||
@SerializedName("rew")
|
||||
public ButtonDTO rewind;
|
||||
|
||||
/**
|
||||
* Indicates if repeat button is enabled/disabled,
|
||||
* or if there is a custom button definition.
|
||||
*/
|
||||
@SerializedName("repeat")
|
||||
public ButtonDTO repeat;
|
||||
|
||||
/**
|
||||
* Indicates if shuffle button is enabled/disabled,
|
||||
* or if there is a custom button definition.
|
||||
*/
|
||||
@SerializedName("shuffle")
|
||||
public ButtonDTO shuffle;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link RemoteMetaDTO} contains remote metadata information, including button and
|
||||
* button override functionality.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class RemoteMetaDTO {
|
||||
|
||||
/**
|
||||
* Contains button specifications for forward, rewind, repeat, shuffle
|
||||
*/
|
||||
public ButtonsDTO buttons;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("title")
|
||||
public String title;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("artist")
|
||||
public String artist;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("album")
|
||||
public String album;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("artwork_url")
|
||||
public String artworkUrl;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("coverart")
|
||||
public String coverart;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("coverid")
|
||||
public String coverid;
|
||||
|
||||
/**
|
||||
* Currently unused
|
||||
*/
|
||||
@SerializedName("year")
|
||||
public String year;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link StatusResponseDTO} is the response received from a player status request.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class StatusResponseDTO {
|
||||
|
||||
/**
|
||||
* Id. Currently unused.
|
||||
*/
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* Method name. Normally "slim.request"
|
||||
*/
|
||||
@SerializedName("method")
|
||||
public String method;
|
||||
|
||||
/**
|
||||
* Parameters passed in the query. Currently unused.
|
||||
*/
|
||||
@SerializedName("params")
|
||||
public Object params;
|
||||
|
||||
/**
|
||||
* Contains the result of the query
|
||||
*/
|
||||
@SerializedName("result")
|
||||
public StatusResultDTO result;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.dto;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* The {@link StatusResultDTO} represents the result of a status request.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class StatusResultDTO {
|
||||
|
||||
/**
|
||||
* Remote metadata information, including button definitions/redefinitions.
|
||||
*/
|
||||
@SerializedName("remoteMeta")
|
||||
public RemoteMetaDTO remoteMeta;
|
||||
|
||||
/**
|
||||
* These remaining fields are currently unused by the binding,
|
||||
* as they also are returned by the Command Line Interface (CLI).
|
||||
*/
|
||||
@SerializedName("current_title")
|
||||
public String currentTitle;
|
||||
|
||||
@SerializedName("digital_volume_control")
|
||||
public Integer digitalVolumeControl;
|
||||
|
||||
@SerializedName("duration")
|
||||
public Double duration;
|
||||
|
||||
@SerializedName("mixer volume")
|
||||
public Integer mixerVolume;
|
||||
|
||||
@SerializedName("player_connected")
|
||||
public Integer playerConnected;
|
||||
|
||||
@SerializedName("player_ip")
|
||||
public String playerIpAddress;
|
||||
|
||||
@SerializedName("player_name")
|
||||
public String playerName;
|
||||
|
||||
@SerializedName("playlist mode")
|
||||
public String playlistMode;
|
||||
|
||||
@SerializedName("playlist repeat")
|
||||
public Integer playlistRepeat;
|
||||
|
||||
@SerializedName("playlist shuffle")
|
||||
public Integer playlistShuffle;
|
||||
|
||||
@SerializedName("playlist_cur_index")
|
||||
public String playListCurrentIndex;
|
||||
|
||||
@SerializedName("playlist_timestamp")
|
||||
public String playlistTimestamp;
|
||||
|
||||
@SerializedName("playlist_tracks")
|
||||
public Integer playlistTracks;
|
||||
|
||||
@SerializedName("power")
|
||||
public String power;
|
||||
|
||||
@SerializedName("rate")
|
||||
public String rate;
|
||||
|
||||
@SerializedName("remote")
|
||||
public String remote;
|
||||
|
||||
@SerializedName("repeating_stream")
|
||||
public Integer repeatingStream;
|
||||
|
||||
@SerializedName("seq_no")
|
||||
public Integer sequenceNumber;
|
||||
|
||||
@SerializedName("signalstrength")
|
||||
public Integer signalStrength;
|
||||
|
||||
@SerializedName("time")
|
||||
public String time;
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.openhab.binding.squeezebox.internal.model.Favorite;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This {@link SqueezeBoxNotificationListener}- is a type of PlayerEventListener
|
||||
* that's used to monitor certain events related to the notification functionality.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
* @author Mark Hilbush - Added event to update favorites list
|
||||
*/
|
||||
public final class SqueezeBoxNotificationListener implements SqueezeBoxPlayerEventListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxNotificationListener.class);
|
||||
|
||||
private final String playerMAC;
|
||||
|
||||
// Used to monitor when the player stops
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
private final AtomicBoolean stopped = new AtomicBoolean(false);
|
||||
|
||||
// Used to monitor when the player pauses
|
||||
private final AtomicBoolean paused = new AtomicBoolean(false);
|
||||
|
||||
// Used to monitor for updates to the playlist
|
||||
private final AtomicBoolean playlistUpdated = new AtomicBoolean(false);
|
||||
|
||||
// Used to monitor when the player volume changes to a specific target value
|
||||
private final AtomicInteger volume = new AtomicInteger(-1);
|
||||
|
||||
SqueezeBoxNotificationListener(String playerMAC) {
|
||||
this.playerMAC = playerMAC;
|
||||
}
|
||||
|
||||
// Stopped
|
||||
public void resetStopped() {
|
||||
this.started.set(false);
|
||||
this.stopped.set(false);
|
||||
}
|
||||
|
||||
public boolean isStopped() {
|
||||
return this.stopped.get();
|
||||
}
|
||||
|
||||
// Paused
|
||||
public void resetPaused() {
|
||||
this.paused.set(false);
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return this.paused.get();
|
||||
}
|
||||
|
||||
// Playlist updated
|
||||
public void resetPlaylistUpdated() {
|
||||
this.playlistUpdated.set(false);
|
||||
}
|
||||
|
||||
public boolean isPlaylistUpdated() {
|
||||
return this.playlistUpdated.get();
|
||||
}
|
||||
|
||||
// Volume updated
|
||||
public void resetVolumeUpdated() {
|
||||
this.volume.set(-1);
|
||||
}
|
||||
|
||||
public boolean isVolumeUpdated(int volume) {
|
||||
return this.volume.get() == volume;
|
||||
}
|
||||
|
||||
// Implementation of listener interfaces
|
||||
@Override
|
||||
public void playerAdded(SqueezeBoxPlayer player) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerChangeEvent(String mac, boolean power) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Monitor for player mode changing to stop.
|
||||
*/
|
||||
@Override
|
||||
public void modeChangeEvent(String mac, String mode) {
|
||||
if (!this.playerMAC.equals(mac)) {
|
||||
return;
|
||||
}
|
||||
logger.trace("Mode is {} for player {}", mode, mac);
|
||||
|
||||
if (mode.equals("play")) {
|
||||
this.started.set(true);
|
||||
} else if (this.started.get() && mode.equals("stop")) {
|
||||
this.stopped.set(true);
|
||||
}
|
||||
if (mode.equals("pause")) {
|
||||
this.paused.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Monitor for when the volume is updated to a specific target value
|
||||
*/
|
||||
@Override
|
||||
public void absoluteVolumeChangeEvent(String mac, int volume) {
|
||||
if (!this.playerMAC.equals(mac)) {
|
||||
return;
|
||||
}
|
||||
this.volume.set(volume);
|
||||
logger.trace("Volume is {} for player {}", volume, mac);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
|
||||
if (!this.playerMAC.equals(mac)) {
|
||||
return;
|
||||
}
|
||||
|
||||
int newVolume = this.volume.get() + volumeChange;
|
||||
newVolume = Math.min(newVolume, 100);
|
||||
newVolume = Math.max(newVolume, 0);
|
||||
|
||||
this.volume.set(newVolume);
|
||||
logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, volume);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void muteChangeEvent(String mac, boolean mute) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistIndexEvent(String mac, int index) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlayingTimeEvent(String mac, int time) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void durationEvent(String mac, int duration) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Monitor for when the playlist is updated
|
||||
*/
|
||||
@Override
|
||||
public void numberPlaylistTracksEvent(String mac, int track) {
|
||||
if (!this.playerMAC.equals(mac)) {
|
||||
return;
|
||||
}
|
||||
logger.trace("Number of playlist tracks is {} for player {}", track, mac);
|
||||
playlistUpdated.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistRepeatEvent(String mac, int repeat) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void titleChangeEvent(String mac, String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void albumChangeEvent(String mac, String album) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void artistChangeEvent(String mac, String artist) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void coverArtChangeEvent(String mac, String coverArtUrl) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void yearChangeEvent(String mac, String year) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void genreChangeEvent(String mac, String genre) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteTitleChangeEvent(String mac, String title) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void irCodeChangeEvent(String mac, String ircode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFavoritesListEvent(List<Favorite> favorites) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sourceChangeEvent(String mac, String source) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/***
|
||||
* Utility class to play a notification message. The message is added
|
||||
* to the playlist, played and the previous state of the playlist and the
|
||||
* player is restored.
|
||||
*
|
||||
* @author Mark Hilbush - Initial Contribution
|
||||
* @author Patrik Gfeller - Utility class added reduce complexity and length of SqueezeBoxPlayerHandler.java
|
||||
* @author Mark Hilbush - Convert sound notification volume from channel to config parameter
|
||||
*
|
||||
*/
|
||||
class SqueezeBoxNotificationPlayer implements Closeable {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxNotificationPlayer.class);
|
||||
|
||||
// An exception is thrown if we do not receive an acknowledge
|
||||
// for a volume set command in the given amount of time [s].
|
||||
private static final int VOLUME_COMMAND_TIMEOUT = 4;
|
||||
|
||||
// We expect the media server to acknowledge a playlist command.
|
||||
// An exception is thrown if the playlist command was not processed
|
||||
// after the defined amount in [s]
|
||||
private static final int PLAYLIST_COMMAND_TIMEOUT = 5;
|
||||
|
||||
private final SqueezeBoxPlayerState playerState;
|
||||
private final SqueezeBoxPlayerHandler squeezeBoxPlayerHandler;
|
||||
private final SqueezeBoxServerHandler squeezeBoxServerHandler;
|
||||
private final StringType uri;
|
||||
private final String mac;
|
||||
|
||||
boolean playlistModified;
|
||||
|
||||
private int notificationMessagePlaylistsIndex;
|
||||
|
||||
SqueezeBoxNotificationPlayer(SqueezeBoxPlayerHandler squeezeBoxPlayerHandler,
|
||||
SqueezeBoxServerHandler squeezeBoxServerHandler, StringType uri) {
|
||||
this.squeezeBoxPlayerHandler = squeezeBoxPlayerHandler;
|
||||
this.squeezeBoxServerHandler = squeezeBoxServerHandler;
|
||||
this.mac = squeezeBoxPlayerHandler.getMac();
|
||||
this.uri = uri;
|
||||
this.playerState = new SqueezeBoxPlayerState(squeezeBoxPlayerHandler);
|
||||
}
|
||||
|
||||
void play() throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
if (squeezeBoxServerHandler == null) {
|
||||
logger.warn("Server handler is null");
|
||||
return;
|
||||
}
|
||||
setupPlayerForNotification();
|
||||
addNotificationMessageToPlaylist();
|
||||
playNotification();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
restorePlayerState();
|
||||
}
|
||||
|
||||
private void setupPlayerForNotification() throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
logger.debug("Setting up player for notification");
|
||||
if (!playerState.isPoweredOn()) {
|
||||
logger.debug("Powering on the player");
|
||||
squeezeBoxServerHandler.powerOn(mac);
|
||||
}
|
||||
if (playerState.isShuffling()) {
|
||||
logger.debug("Turning off shuffle");
|
||||
squeezeBoxServerHandler.setShuffleMode(mac, 0);
|
||||
}
|
||||
if (playerState.isRepeating()) {
|
||||
logger.debug("Turning off repeat");
|
||||
squeezeBoxServerHandler.setRepeatMode(mac, 0);
|
||||
}
|
||||
if (playerState.isPlaying()) {
|
||||
squeezeBoxServerHandler.stop(mac);
|
||||
}
|
||||
setVolume(squeezeBoxPlayerHandler.getNotificationSoundVolume().intValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a volume set command if target volume is not equal to the current volume.
|
||||
*
|
||||
* @param requestedVolume The requested volume value.
|
||||
* @throws InterruptedException Thread interrupted during while we were waiting for an answer from the media server.
|
||||
* @throws SqueezeBoxTimeoutException Volume command was not acknowledged by the media server.
|
||||
*/
|
||||
private void setVolume(int requestedVolume) throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
if (playerState.getVolume() == requestedVolume) {
|
||||
return;
|
||||
}
|
||||
|
||||
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
|
||||
listener.resetVolumeUpdated();
|
||||
|
||||
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
|
||||
squeezeBoxServerHandler.setVolume(mac, requestedVolume);
|
||||
|
||||
logger.trace("Waiting up to {} s for volume to be updated...", VOLUME_COMMAND_TIMEOUT);
|
||||
|
||||
try {
|
||||
int timeoutCount = 0;
|
||||
|
||||
while (!listener.isVolumeUpdated(requestedVolume)) {
|
||||
Thread.sleep(100);
|
||||
if (timeoutCount++ > VOLUME_COMMAND_TIMEOUT * 10) {
|
||||
throw new SqueezeBoxTimeoutException("Unable to update volume.");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void addNotificationMessageToPlaylist() throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
logger.debug("Adding notification message to playlist");
|
||||
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
|
||||
listener.resetPlaylistUpdated();
|
||||
|
||||
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
|
||||
squeezeBoxServerHandler.addPlaylistItem(mac, uri.toString(), "Notification");
|
||||
|
||||
try {
|
||||
updatePlaylist(listener);
|
||||
this.playlistModified = true;
|
||||
} finally {
|
||||
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeNotificationMessageFromPlaylist() throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
logger.debug("Removing notification message from playlist");
|
||||
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
|
||||
listener.resetPlaylistUpdated();
|
||||
|
||||
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
|
||||
squeezeBoxServerHandler.deletePlaylistItem(mac, notificationMessagePlaylistsIndex);
|
||||
|
||||
try {
|
||||
updatePlaylist(listener);
|
||||
} finally {
|
||||
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Monitor the number of playlist entries. When it changes, then we know the playlist
|
||||
* has been updated with the notification URL. There's probably an edge case here where
|
||||
* someone is updating the playlist at the same time, but that should be rare.
|
||||
*
|
||||
* @param listener
|
||||
* @throws InterruptedException
|
||||
* @throws SqueezeBoxTimeoutException
|
||||
*/
|
||||
private void updatePlaylist(SqueezeBoxNotificationListener listener)
|
||||
throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
logger.trace("Waiting up to {} s for playlist to be updated...", PLAYLIST_COMMAND_TIMEOUT);
|
||||
|
||||
int timeoutCount = 0;
|
||||
|
||||
while (!listener.isPlaylistUpdated()) {
|
||||
Thread.sleep(100);
|
||||
if (timeoutCount++ > PLAYLIST_COMMAND_TIMEOUT * 10) {
|
||||
logger.debug("Update playlist timed out after {} seconds", PLAYLIST_COMMAND_TIMEOUT);
|
||||
throw new SqueezeBoxTimeoutException("Unable to update playlist.");
|
||||
}
|
||||
}
|
||||
logger.debug("Playlist updated");
|
||||
}
|
||||
|
||||
private void playNotification() throws InterruptedException, SqueezeBoxTimeoutException {
|
||||
logger.debug("Playing notification");
|
||||
|
||||
notificationMessagePlaylistsIndex = squeezeBoxPlayerHandler.currentNumberPlaylistTracks() - 1;
|
||||
SqueezeBoxNotificationListener listener = new SqueezeBoxNotificationListener(mac);
|
||||
listener.resetStopped();
|
||||
|
||||
squeezeBoxServerHandler.registerSqueezeBoxPlayerListener(listener);
|
||||
squeezeBoxServerHandler.playPlaylistItem(mac, notificationMessagePlaylistsIndex);
|
||||
|
||||
try {
|
||||
int notificationTimeout = squeezeBoxPlayerHandler.getNotificationTimeout();
|
||||
int timeoutCount = 0;
|
||||
|
||||
logger.trace("Waiting up to {} s for stop...", notificationTimeout);
|
||||
while (!listener.isStopped()) {
|
||||
Thread.sleep(100);
|
||||
if (timeoutCount++ > notificationTimeout * 10) {
|
||||
logger.debug("Notification message timed out after {} seconds", notificationTimeout);
|
||||
throw new SqueezeBoxTimeoutException("Notification message timed out");
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
squeezeBoxServerHandler.unregisterSqueezeBoxPlayerListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePlayerState() {
|
||||
logger.debug("Restoring player state");
|
||||
|
||||
// Mute the player to prevent any noise during the transition to saved state
|
||||
// Don't wait for the volume acknowledge as there´s nothing to do about it at this point.
|
||||
squeezeBoxServerHandler.setVolume(mac, 0);
|
||||
|
||||
if (playlistModified) {
|
||||
try {
|
||||
removeNotificationMessageFromPlaylist();
|
||||
} catch (InterruptedException | SqueezeBoxTimeoutException e) {
|
||||
// Not much we can do here except log it and continue on
|
||||
logger.debug("Exception while removing notification from playlist: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Resume playing saved playlist item.
|
||||
// Note that setting the time doesn't work for remote streams.
|
||||
squeezeBoxServerHandler.playPlaylistItem(mac, playerState.getPlaylistIndex());
|
||||
squeezeBoxServerHandler.setPlayingTime(mac, playerState.getPlayingTime());
|
||||
|
||||
switch (playerState.getPlayState()) {
|
||||
case PLAY:
|
||||
logger.debug("Resuming last item playing");
|
||||
break;
|
||||
case PAUSE:
|
||||
/*
|
||||
* If the player was paused, stop it. We stop it because the LMS
|
||||
* doesn't respond to a pause command while it's processing the
|
||||
* above 'playPlaylist item' command. The consequence of this is
|
||||
* we lose the ability to resume local music from saved playing time.
|
||||
*/
|
||||
logger.debug("Stopping the player");
|
||||
squeezeBoxServerHandler.stop(mac);
|
||||
break;
|
||||
case STOP:
|
||||
logger.debug("Stopping the player");
|
||||
squeezeBoxServerHandler.stop(mac);
|
||||
break;
|
||||
}
|
||||
|
||||
// Restore the saved volume level
|
||||
squeezeBoxServerHandler.setVolume(mac, playerState.getVolume());
|
||||
|
||||
if (playerState.isShuffling()) {
|
||||
logger.debug("Restoring shuffle mode");
|
||||
squeezeBoxServerHandler.setShuffleMode(mac, playerState.getShuffle());
|
||||
}
|
||||
if (playerState.isRepeating()) {
|
||||
logger.debug("Restoring repeat mode");
|
||||
squeezeBoxServerHandler.setRepeatMode(mac, playerState.getRepeat());
|
||||
}
|
||||
if (playerState.isMuted()) {
|
||||
logger.debug("Re-muting the player");
|
||||
squeezeBoxServerHandler.mute(mac);
|
||||
}
|
||||
if (!playerState.isPoweredOn()) {
|
||||
logger.debug("Powering off the player");
|
||||
squeezeBoxServerHandler.powerOff(mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
/**
|
||||
* Represents a Squeeze Player
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class SqueezeBoxPlayer {
|
||||
public String macAddress;
|
||||
public String name;
|
||||
public String ipAddr;
|
||||
public String model;
|
||||
public String uuid;
|
||||
|
||||
public SqueezeBoxPlayer() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* UID of player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* UID of player
|
||||
*
|
||||
* @param uuid
|
||||
*/
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mac Address of player
|
||||
*
|
||||
* @param macAddress
|
||||
*/
|
||||
public String getMacAddress() {
|
||||
return macAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mac Address of player
|
||||
*
|
||||
* @param macAddress
|
||||
*/
|
||||
public void setMacAddress(String macAddress) {
|
||||
this.macAddress = macAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name (label) of a player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name (label) of a player
|
||||
*
|
||||
* @param name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ip address of a player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getIpAddr() {
|
||||
return ipAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ip address of a player
|
||||
*
|
||||
* @param ipAddr
|
||||
*/
|
||||
public void setIpAddr(String ipAddr) {
|
||||
this.ipAddr = ipAddr;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of player
|
||||
*
|
||||
* @param model
|
||||
*/
|
||||
public void setModel(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.openhab.binding.squeezebox.internal.model.Favorite;
|
||||
|
||||
/**
|
||||
* @author Markus Wolters - Initial contribution
|
||||
* @author Ben Jones - ?
|
||||
* @author Dan Cunningham - OH2 port
|
||||
* @author Mark Hilbush - Added durationEvent
|
||||
* @author Mark Hilbush - Added event to update favorites list
|
||||
*/
|
||||
public interface SqueezeBoxPlayerEventListener {
|
||||
|
||||
void playerAdded(SqueezeBoxPlayer player);
|
||||
|
||||
void powerChangeEvent(String mac, boolean power);
|
||||
|
||||
void modeChangeEvent(String mac, String mode);
|
||||
|
||||
/**
|
||||
* Reports a new absolute volume for a given player.
|
||||
*
|
||||
* @param mac
|
||||
* @param volume
|
||||
*/
|
||||
void absoluteVolumeChangeEvent(String mac, int volume);
|
||||
|
||||
/**
|
||||
* Reports a relative volume change for a given player.
|
||||
*
|
||||
* @param mac
|
||||
* @param volumeChange
|
||||
*/
|
||||
void relativeVolumeChangeEvent(String mac, int volumeChange);
|
||||
|
||||
void muteChangeEvent(String mac, boolean mute);
|
||||
|
||||
void currentPlaylistIndexEvent(String mac, int index);
|
||||
|
||||
void currentPlayingTimeEvent(String mac, int time);
|
||||
|
||||
void durationEvent(String mac, int duration);
|
||||
|
||||
void numberPlaylistTracksEvent(String mac, int track);
|
||||
|
||||
void currentPlaylistShuffleEvent(String mac, int shuffle);
|
||||
|
||||
void currentPlaylistRepeatEvent(String mac, int repeat);
|
||||
|
||||
void titleChangeEvent(String mac, String title);
|
||||
|
||||
void albumChangeEvent(String mac, String album);
|
||||
|
||||
void artistChangeEvent(String mac, String artist);
|
||||
|
||||
void coverArtChangeEvent(String mac, String coverArtUrl);
|
||||
|
||||
void yearChangeEvent(String mac, String year);
|
||||
|
||||
void genreChangeEvent(String mac, String genre);
|
||||
|
||||
void remoteTitleChangeEvent(String mac, String title);
|
||||
|
||||
void irCodeChangeEvent(String mac, String ircode);
|
||||
|
||||
void updateFavoritesListEvent(List<Favorite> favorites);
|
||||
|
||||
void sourceChangeEvent(String mac, String source);
|
||||
|
||||
void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand);
|
||||
}
|
||||
@@ -0,0 +1,728 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.eclipse.jdt.annotation.NonNull;
|
||||
import org.openhab.binding.squeezebox.internal.SqueezeBoxStateDescriptionOptionsProvider;
|
||||
import org.openhab.binding.squeezebox.internal.config.SqueezeBoxPlayerConfig;
|
||||
import org.openhab.binding.squeezebox.internal.model.Favorite;
|
||||
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
|
||||
import org.openhab.core.cache.ExpiringCacheMap;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.IncreaseDecreaseType;
|
||||
import org.openhab.core.library.types.NextPreviousType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.PlayPauseType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.RewindFastforwardType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
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.ThingStatusInfo;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SqueezeBoxPlayerHandler} is responsible for handling states, which
|
||||
* are sent to/from channels.
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Mark Hilbush - Improved handling of player status, prevent REFRESH from causing exception
|
||||
* @author Mark Hilbush - Implement AudioSink and notifications
|
||||
* @author Mark Hilbush - Added duration channel
|
||||
* @author Patrik Gfeller - Timeout for TTS messages increased from 30 to 90s.
|
||||
* @author Mark Hilbush - Get favorites from server and play favorite
|
||||
* @author Mark Hilbush - Convert sound notification volume from channel to config parameter
|
||||
* @author Mark Hilbush - Add like/unlike functionality
|
||||
*/
|
||||
public class SqueezeBoxPlayerHandler extends BaseThingHandler implements SqueezeBoxPlayerEventListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerHandler.class);
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
|
||||
.singleton(SQUEEZEBOXPLAYER_THING_TYPE);
|
||||
|
||||
/**
|
||||
* We need to remember some states to change offsets in volume, time index,
|
||||
* etc..
|
||||
*/
|
||||
protected Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
/**
|
||||
* Keeps current track time
|
||||
*/
|
||||
private ScheduledFuture<?> timeCounterJob;
|
||||
|
||||
/**
|
||||
* Local reference to our bridge
|
||||
*/
|
||||
private SqueezeBoxServerHandler squeezeBoxServerHandler;
|
||||
|
||||
/**
|
||||
* Our mac address, needed everywhere
|
||||
*/
|
||||
private String mac;
|
||||
|
||||
/**
|
||||
* The server sends us the current time on play/pause/stop events, we
|
||||
* increment it locally from there on
|
||||
*/
|
||||
private int currentTime = 0;
|
||||
|
||||
/**
|
||||
* Our we playing something right now or not, need to keep current track
|
||||
* time
|
||||
*/
|
||||
private boolean playing;
|
||||
|
||||
/**
|
||||
* Separate volume level for notifications
|
||||
*/
|
||||
private Integer notificationSoundVolume = null;
|
||||
|
||||
private String callbackUrl;
|
||||
|
||||
private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;
|
||||
|
||||
private static final ExpiringCacheMap<String, RawType> IMAGE_CACHE = new ExpiringCacheMap<>(
|
||||
TimeUnit.MINUTES.toMillis(15)); // 15min
|
||||
|
||||
private String likeCommand;
|
||||
private String unlikeCommand;
|
||||
|
||||
/**
|
||||
* Creates SqueezeBox Player Handler
|
||||
*
|
||||
* @param thing
|
||||
* @param stateDescriptionProvider
|
||||
*/
|
||||
public SqueezeBoxPlayerHandler(@NonNull Thing thing, String callbackUrl,
|
||||
SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) {
|
||||
super(thing);
|
||||
this.callbackUrl = callbackUrl;
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac;
|
||||
timeCounter();
|
||||
updateBridgeStatus();
|
||||
logger.debug("player thing {} initialized with mac {}", getThing().getUID(), mac);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
|
||||
updateBridgeStatus();
|
||||
}
|
||||
|
||||
private void updateBridgeStatus() {
|
||||
Thing bridge = getBridge();
|
||||
if (bridge != null) {
|
||||
squeezeBoxServerHandler = (SqueezeBoxServerHandler) bridge.getHandler();
|
||||
ThingStatus bridgeStatus = bridge.getStatus();
|
||||
if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) {
|
||||
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
|
||||
} else if (bridgeStatus == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not found");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// stop our duration counter
|
||||
if (timeCounterJob != null && !timeCounterJob.isCancelled()) {
|
||||
timeCounterJob.cancel(true);
|
||||
timeCounterJob = null;
|
||||
}
|
||||
|
||||
if (squeezeBoxServerHandler != null) {
|
||||
squeezeBoxServerHandler.removePlayerCache(mac);
|
||||
}
|
||||
logger.debug("player thing {} disposed for mac {}", getThing().getUID(), mac);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (squeezeBoxServerHandler == null) {
|
||||
logger.debug("Player {} has no server configured, ignoring command: {}", getThing().getUID(), command);
|
||||
return;
|
||||
}
|
||||
// Some of the code below is not designed to handle REFRESH, only reply to channels where cached values exist
|
||||
if (command == RefreshType.REFRESH) {
|
||||
String channelID = channelUID.getId();
|
||||
if (stateMap.containsKey(channelID)) {
|
||||
updateState(channelID, stateMap.get(channelID));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getIdWithoutGroup()) {
|
||||
case CHANNEL_POWER:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.powerOn(mac);
|
||||
} else {
|
||||
squeezeBoxServerHandler.powerOff(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_MUTE:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.mute(mac);
|
||||
} else {
|
||||
squeezeBoxServerHandler.unMute(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_STOP:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.stop(mac);
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
squeezeBoxServerHandler.play(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_PLAY_PAUSE:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.play(mac);
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
squeezeBoxServerHandler.pause(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_PREV:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.prev(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_NEXT:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.next(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_VOLUME:
|
||||
if (command instanceof PercentType) {
|
||||
squeezeBoxServerHandler.setVolume(mac, ((PercentType) command).intValue());
|
||||
} else if (command.equals(IncreaseDecreaseType.INCREASE)) {
|
||||
squeezeBoxServerHandler.volumeUp(mac, currentVolume());
|
||||
} else if (command.equals(IncreaseDecreaseType.DECREASE)) {
|
||||
squeezeBoxServerHandler.volumeDown(mac, currentVolume());
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
squeezeBoxServerHandler.mute(mac);
|
||||
} else if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.unMute(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_CONTROL:
|
||||
if (command instanceof PlayPauseType) {
|
||||
if (command.equals(PlayPauseType.PLAY)) {
|
||||
squeezeBoxServerHandler.play(mac);
|
||||
} else if (command.equals(PlayPauseType.PAUSE)) {
|
||||
squeezeBoxServerHandler.pause(mac);
|
||||
}
|
||||
}
|
||||
if (command instanceof NextPreviousType) {
|
||||
if (command.equals(NextPreviousType.NEXT)) {
|
||||
squeezeBoxServerHandler.next(mac);
|
||||
} else if (command.equals(NextPreviousType.PREVIOUS)) {
|
||||
squeezeBoxServerHandler.prev(mac);
|
||||
}
|
||||
}
|
||||
if (command instanceof RewindFastforwardType) {
|
||||
if (command.equals(RewindFastforwardType.REWIND)) {
|
||||
squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() - 5);
|
||||
} else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
|
||||
squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() + 5);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_STREAM:
|
||||
squeezeBoxServerHandler.playUrl(mac, command.toString());
|
||||
break;
|
||||
case CHANNEL_SYNC:
|
||||
if (StringUtils.isBlank(command.toString())) {
|
||||
squeezeBoxServerHandler.unSyncPlayer(mac);
|
||||
} else {
|
||||
squeezeBoxServerHandler.syncPlayer(mac, command.toString());
|
||||
}
|
||||
break;
|
||||
case CHANNEL_UNSYNC:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.unSyncPlayer(mac);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_PLAYLIST_INDEX:
|
||||
squeezeBoxServerHandler.playPlaylistItem(mac, ((DecimalType) command).intValue());
|
||||
break;
|
||||
case CHANNEL_CURRENT_PLAYING_TIME:
|
||||
squeezeBoxServerHandler.setPlayingTime(mac, ((DecimalType) command).intValue());
|
||||
break;
|
||||
case CHANNEL_CURRENT_PLAYLIST_SHUFFLE:
|
||||
squeezeBoxServerHandler.setShuffleMode(mac, ((DecimalType) command).intValue());
|
||||
break;
|
||||
case CHANNEL_CURRENT_PLAYLIST_REPEAT:
|
||||
squeezeBoxServerHandler.setRepeatMode(mac, ((DecimalType) command).intValue());
|
||||
break;
|
||||
case CHANNEL_FAVORITES_PLAY:
|
||||
squeezeBoxServerHandler.playFavorite(mac, command.toString());
|
||||
break;
|
||||
case CHANNEL_RATE:
|
||||
if (command.equals(OnOffType.ON)) {
|
||||
squeezeBoxServerHandler.rate(mac, likeCommand);
|
||||
} else if (command.equals(OnOffType.OFF)) {
|
||||
squeezeBoxServerHandler.rate(mac, unlikeCommand);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playerAdded(SqueezeBoxPlayer player) {
|
||||
// Player properties are saved in SqueezeBoxPlayerDiscoveryParticipant
|
||||
}
|
||||
|
||||
@Override
|
||||
public void powerChangeEvent(String mac, boolean power) {
|
||||
updateChannel(mac, CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
|
||||
if (!power && isMe(mac)) {
|
||||
playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void modeChangeEvent(String mac, String mode) {
|
||||
updateChannel(mac, CHANNEL_CONTROL, "play".equals(mode) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
|
||||
updateChannel(mac, CHANNEL_PLAY_PAUSE, "play".equals(mode) ? OnOffType.ON : OnOffType.OFF);
|
||||
updateChannel(mac, CHANNEL_STOP, "stop".equals(mode) ? OnOffType.ON : OnOffType.OFF);
|
||||
if (isMe(mac)) {
|
||||
playing = "play".equalsIgnoreCase(mode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sourceChangeEvent(String mac, String source) {
|
||||
updateChannel(mac, CHANNEL_SOURCE, StringType.valueOf(source));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void absoluteVolumeChangeEvent(String mac, int volume) {
|
||||
int newVolume = volume;
|
||||
newVolume = Math.min(100, newVolume);
|
||||
newVolume = Math.max(0, newVolume);
|
||||
updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void relativeVolumeChangeEvent(String mac, int volumeChange) {
|
||||
int newVolume = currentVolume() + volumeChange;
|
||||
newVolume = Math.min(100, newVolume);
|
||||
newVolume = Math.max(0, newVolume);
|
||||
updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
|
||||
|
||||
if (isMe(mac)) {
|
||||
logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, newVolume);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void muteChangeEvent(String mac, boolean mute) {
|
||||
updateChannel(mac, CHANNEL_MUTE, mute ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistIndexEvent(String mac, int index) {
|
||||
updateChannel(mac, CHANNEL_PLAYLIST_INDEX, new DecimalType(index));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlayingTimeEvent(String mac, int time) {
|
||||
updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(time));
|
||||
if (isMe(mac)) {
|
||||
currentTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void durationEvent(String mac, int duration) {
|
||||
if (getThing().getChannel(CHANNEL_DURATION) == null) {
|
||||
logger.debug("Channel 'duration' does not exist. Delete and readd player thing to pick up channel.");
|
||||
return;
|
||||
}
|
||||
updateChannel(mac, CHANNEL_DURATION, new DecimalType(duration));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void numberPlaylistTracksEvent(String mac, int track) {
|
||||
updateChannel(mac, CHANNEL_NUMBER_PLAYLIST_TRACKS, new DecimalType(track));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistShuffleEvent(String mac, int shuffle) {
|
||||
updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_SHUFFLE, new DecimalType(shuffle));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void currentPlaylistRepeatEvent(String mac, int repeat) {
|
||||
updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_REPEAT, new DecimalType(repeat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void titleChangeEvent(String mac, String title) {
|
||||
updateChannel(mac, CHANNEL_TITLE, new StringType(title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void albumChangeEvent(String mac, String album) {
|
||||
updateChannel(mac, CHANNEL_ALBUM, new StringType(album));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void artistChangeEvent(String mac, String artist) {
|
||||
updateChannel(mac, CHANNEL_ARTIST, new StringType(artist));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void coverArtChangeEvent(String mac, String coverArtUrl) {
|
||||
updateChannel(mac, CHANNEL_COVERART_DATA, createImage(downloadImage(mac, coverArtUrl)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and cache the image data from an URL.
|
||||
*
|
||||
* @param url The URL of the image to be downloaded.
|
||||
* @return A RawType object containing the image, null if the content type could not be found or the content type is
|
||||
* not an image.
|
||||
*/
|
||||
private RawType downloadImage(String mac, String url) {
|
||||
// Only get the image if this is my PlayerHandler instance
|
||||
if (isMe(mac)) {
|
||||
if (StringUtils.isNotEmpty(url)) {
|
||||
String sanitizedUrl = sanitizeUrl(url);
|
||||
RawType image = IMAGE_CACHE.putIfAbsentAndGet(url, () -> {
|
||||
logger.debug("Trying to download the content of URL {}", sanitizedUrl);
|
||||
try {
|
||||
return HttpUtil.downloadImage(url);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("IllegalArgumentException when downloading image from {}", sanitizedUrl, e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
if (image == null) {
|
||||
logger.debug("Failed to download the content of URL {}", sanitizedUrl);
|
||||
return null;
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Replaces the password in the URL, if present
|
||||
*/
|
||||
private String sanitizeUrl(String url) {
|
||||
String sanitizedUrl = url;
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
String userInfo = uri.getUserInfo();
|
||||
if (userInfo != null) {
|
||||
String[] userInfoParts = userInfo.split(":");
|
||||
if (userInfoParts.length == 2) {
|
||||
sanitizedUrl = url.replace(userInfoParts[1], "**********");
|
||||
}
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
// Just return what was passed in
|
||||
}
|
||||
return sanitizedUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
|
||||
*/
|
||||
private State createImage(RawType image) {
|
||||
if (image == null) {
|
||||
return UnDefType.UNDEF;
|
||||
} else {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void yearChangeEvent(String mac, String year) {
|
||||
updateChannel(mac, CHANNEL_YEAR, new StringType(year));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void genreChangeEvent(String mac, String genre) {
|
||||
updateChannel(mac, CHANNEL_GENRE, new StringType(genre));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remoteTitleChangeEvent(String mac, String title) {
|
||||
updateChannel(mac, CHANNEL_REMOTE_TITLE, new StringType(title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void irCodeChangeEvent(String mac, String ircode) {
|
||||
if (isMe(mac)) {
|
||||
postCommand(CHANNEL_IRCODE, new StringType(ircode));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
|
||||
if (isMe(mac)) {
|
||||
this.likeCommand = likeCommand;
|
||||
this.unlikeCommand = unlikeCommand;
|
||||
logger.trace("Player {} got a button change event: like='{}' unlike='{}'", mac, likeCommand, unlikeCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFavoritesListEvent(List<Favorite> favorites) {
|
||||
logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size());
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
for (Favorite favorite : favorites) {
|
||||
options.add(new StateOption(favorite.shortId, favorite.name));
|
||||
}
|
||||
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FAVORITES_PLAY), options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a channel if the mac matches our own
|
||||
*
|
||||
* @param mac
|
||||
* @param channelID
|
||||
* @param state
|
||||
*/
|
||||
private void updateChannel(String mac, String channelID, State state) {
|
||||
if (isMe(mac)) {
|
||||
State prevState = stateMap.put(channelID, state);
|
||||
if (prevState == null || !prevState.equals(state)) {
|
||||
logger.trace("Updating channel {} for thing {} with mac {} to state {}", channelID, getThing().getUID(),
|
||||
mac, state);
|
||||
updateState(channelID, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods to get the current state of the player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
int currentVolume() {
|
||||
if (stateMap.containsKey(CHANNEL_VOLUME)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_VOLUME)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int currentPlayingTime() {
|
||||
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYING_TIME)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYING_TIME)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int currentNumberPlaylistTracks() {
|
||||
if (stateMap.containsKey(CHANNEL_NUMBER_PLAYLIST_TRACKS)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_NUMBER_PLAYLIST_TRACKS)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int currentPlaylistIndex() {
|
||||
if (stateMap.containsKey(CHANNEL_PLAYLIST_INDEX)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_PLAYLIST_INDEX)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
boolean currentPower() {
|
||||
if (stateMap.containsKey(CHANNEL_POWER)) {
|
||||
return (stateMap.get(CHANNEL_POWER).equals(OnOffType.ON) ? true : false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean currentStop() {
|
||||
if (stateMap.containsKey(CHANNEL_STOP)) {
|
||||
return (stateMap.get(CHANNEL_STOP).equals(OnOffType.ON) ? true : false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean currentControl() {
|
||||
if (stateMap.containsKey(CHANNEL_CONTROL)) {
|
||||
return (stateMap.get(CHANNEL_CONTROL).equals(PlayPauseType.PLAY) ? true : false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
boolean currentMute() {
|
||||
if (stateMap.containsKey(CHANNEL_MUTE)) {
|
||||
return (stateMap.get(CHANNEL_MUTE).equals(OnOffType.ON) ? true : false);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int currentShuffle() {
|
||||
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
int currentRepeat() {
|
||||
if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_REPEAT)) {
|
||||
return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_REPEAT)).intValue();
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks away when in a play state to keep current track time
|
||||
*/
|
||||
private void timeCounter() {
|
||||
timeCounterJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
if (playing) {
|
||||
updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(currentTime++));
|
||||
}
|
||||
}, 0, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private boolean isMe(String mac) {
|
||||
return mac.equals(this.mac);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns our server handler if set
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SqueezeBoxServerHandler getSqueezeBoxServerHandler() {
|
||||
return this.squeezeBoxServerHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MAC address for this player
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getMac() {
|
||||
return this.mac;
|
||||
}
|
||||
|
||||
/*
|
||||
* Give the notification player access to the notification timeout
|
||||
*/
|
||||
public int getNotificationTimeout() {
|
||||
return getConfigAs(SqueezeBoxPlayerConfig.class).notificationTimeout;
|
||||
}
|
||||
|
||||
/*
|
||||
* Used by the AudioSink to get the volume level that should be used for the notification.
|
||||
* Priority for determining volume is:
|
||||
* - volume is provided in the say/playSound actions
|
||||
* - volume is contained in the player thing's configuration
|
||||
* - current player volume setting
|
||||
*/
|
||||
public PercentType getNotificationSoundVolume() {
|
||||
// Get the notification sound volume from this player thing's configuration
|
||||
Integer configNotificationSoundVolume = getConfigAs(SqueezeBoxPlayerConfig.class).notificationVolume;
|
||||
|
||||
// Determine which volume to use
|
||||
Integer currentNotificationSoundVolume;
|
||||
if (notificationSoundVolume != null) {
|
||||
currentNotificationSoundVolume = notificationSoundVolume;
|
||||
} else if (configNotificationSoundVolume != null) {
|
||||
currentNotificationSoundVolume = configNotificationSoundVolume;
|
||||
} else {
|
||||
currentNotificationSoundVolume = Integer.valueOf(currentVolume());
|
||||
}
|
||||
return new PercentType(currentNotificationSoundVolume.intValue());
|
||||
}
|
||||
|
||||
/*
|
||||
* Used by the AudioSink to set the volume level that should be used to play the notification
|
||||
*/
|
||||
public void setNotificationSoundVolume(PercentType newNotificationSoundVolume) {
|
||||
if (newNotificationSoundVolume != null) {
|
||||
notificationSoundVolume = Integer.valueOf(newNotificationSoundVolume.intValue());
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Play the notification.
|
||||
*/
|
||||
public void playNotificationSoundURI(StringType uri) {
|
||||
logger.debug("Play notification sound on player {} at URI {}", mac, uri);
|
||||
|
||||
try (SqueezeBoxNotificationPlayer notificationPlayer = new SqueezeBoxNotificationPlayer(this,
|
||||
squeezeBoxServerHandler, uri)) {
|
||||
notificationPlayer.play();
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Notification playback was interrupted", e);
|
||||
} catch (SqueezeBoxTimeoutException e) {
|
||||
logger.debug("SqueezeBoxTimeoutException during notification: {}", e.getMessage());
|
||||
} finally {
|
||||
notificationSoundVolume = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Return the IP and port of the OH2 web server
|
||||
*/
|
||||
public String getHostAndPort() {
|
||||
return callbackUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
/***
|
||||
* Enumeration of the play states of a player.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
*
|
||||
*/
|
||||
enum SqueezeBoxPlayerPlayState {
|
||||
STOP,
|
||||
PLAY,
|
||||
PAUSE
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.handler;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SqueezeBoxPlayerState} is responsible for saving the state of a player.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
* @author Patrik Gfeller - Moved class to its own file.
|
||||
*/
|
||||
class SqueezeBoxPlayerState {
|
||||
private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerState.class);
|
||||
|
||||
private boolean savedMute;
|
||||
private boolean savedPower;
|
||||
private boolean savedStop;
|
||||
private boolean savedControl;
|
||||
|
||||
private int savedVolume;
|
||||
private int savedShuffle;
|
||||
private int savedRepeat;
|
||||
private int savedPlaylistIndex;
|
||||
private int savedNumberPlaylistTracks;
|
||||
private int savedPlayingTime;
|
||||
|
||||
private SqueezeBoxPlayerHandler playerHandler;
|
||||
|
||||
public SqueezeBoxPlayerState(SqueezeBoxPlayerHandler playerHandler) {
|
||||
this.playerHandler = playerHandler;
|
||||
save();
|
||||
}
|
||||
|
||||
boolean isMuted() {
|
||||
return savedMute;
|
||||
}
|
||||
|
||||
boolean isPoweredOn() {
|
||||
return savedPower;
|
||||
}
|
||||
|
||||
boolean isStopped() {
|
||||
return savedStop;
|
||||
}
|
||||
|
||||
boolean isPlaying() {
|
||||
return savedControl;
|
||||
}
|
||||
|
||||
boolean isShuffling() {
|
||||
return savedShuffle == 0 ? false : true;
|
||||
}
|
||||
|
||||
int getShuffle() {
|
||||
return savedShuffle;
|
||||
}
|
||||
|
||||
boolean isRepeating() {
|
||||
return savedRepeat == 0 ? false : true;
|
||||
}
|
||||
|
||||
int getRepeat() {
|
||||
return savedRepeat;
|
||||
}
|
||||
|
||||
int getVolume() {
|
||||
return savedVolume;
|
||||
}
|
||||
|
||||
int getPlaylistIndex() {
|
||||
return savedPlaylistIndex;
|
||||
}
|
||||
|
||||
private int getNumberPlaylistTracks() {
|
||||
return savedNumberPlaylistTracks;
|
||||
}
|
||||
|
||||
int getPlayingTime() {
|
||||
return savedPlayingTime;
|
||||
}
|
||||
|
||||
private void save() {
|
||||
savedVolume = playerHandler.currentVolume();
|
||||
savedMute = playerHandler.currentMute();
|
||||
savedPower = playerHandler.currentPower();
|
||||
savedStop = playerHandler.currentStop();
|
||||
savedControl = playerHandler.currentControl();
|
||||
savedShuffle = playerHandler.currentShuffle();
|
||||
savedRepeat = playerHandler.currentRepeat();
|
||||
savedPlaylistIndex = playerHandler.currentPlaylistIndex();
|
||||
savedNumberPlaylistTracks = playerHandler.currentNumberPlaylistTracks();
|
||||
savedPlayingTime = playerHandler.currentPlayingTime();
|
||||
|
||||
logger.debug("Cur State: vol={}, mut={}, pwr={}, stp={}, ctl={}, shf={}, rpt={}, tix={}, tnm={}, tim={}",
|
||||
savedVolume, muteAsString(), powerAsString(), stopAsString(), controlAsString(), shuffleAsString(),
|
||||
repeatAsString(), getPlaylistIndex(), getNumberPlaylistTracks(), getPlayingTime());
|
||||
}
|
||||
|
||||
private String muteAsString() {
|
||||
return isMuted() ? "MUTED" : "NOT MUTED";
|
||||
}
|
||||
|
||||
private String powerAsString() {
|
||||
return isPoweredOn() ? "ON" : "OFF";
|
||||
}
|
||||
|
||||
private String stopAsString() {
|
||||
return isStopped() ? "STOPPED" : "NOT STOPPED";
|
||||
}
|
||||
|
||||
private String controlAsString() {
|
||||
return isPlaying() ? "PLAYING" : "PAUSED";
|
||||
}
|
||||
|
||||
private String shuffleAsString() {
|
||||
String shuffle = "OFF";
|
||||
if (getShuffle() == 1) {
|
||||
shuffle = "SONG";
|
||||
} else if (getShuffle() == 2) {
|
||||
shuffle = "ALBUM";
|
||||
}
|
||||
return shuffle;
|
||||
}
|
||||
|
||||
private String repeatAsString() {
|
||||
String repeat = "OFF";
|
||||
if (getRepeat() == 1) {
|
||||
repeat = "SONG";
|
||||
} else if (getRepeat() == 2) {
|
||||
repeat = "PLAYLIST";
|
||||
}
|
||||
return repeat;
|
||||
}
|
||||
|
||||
/***
|
||||
* Return the player state as {@link SqueezeBoxPlayerPlayState}
|
||||
*
|
||||
* @return {@link SqueezeBoxPlayerPlayState}
|
||||
*/
|
||||
SqueezeBoxPlayerPlayState getPlayState() {
|
||||
if (!isPlaying() && !isStopped()) {
|
||||
return SqueezeBoxPlayerPlayState.PAUSE;
|
||||
}
|
||||
|
||||
if (isPlaying()) {
|
||||
return SqueezeBoxPlayerPlayState.PLAY;
|
||||
}
|
||||
|
||||
return SqueezeBoxPlayerPlayState.STOP;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.model;
|
||||
|
||||
/**
|
||||
* Attributes of a Squeezebox Server favorite
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class Favorite {
|
||||
/**
|
||||
* Favorite id is of form xxxxxxxx.nn
|
||||
*/
|
||||
public String id;
|
||||
|
||||
/**
|
||||
* Just the nn part of the id
|
||||
*/
|
||||
public String shortId;
|
||||
|
||||
/**
|
||||
* The name given to the favorite in the Squeezebox Server.
|
||||
*/
|
||||
public String name;
|
||||
|
||||
/**
|
||||
* Creates a preset from the given favorite id
|
||||
*
|
||||
* @param id Squeezebox Server internal identifier for favorite
|
||||
*/
|
||||
public Favorite(String id) {
|
||||
this.id = id;
|
||||
this.shortId = id;
|
||||
if (id.indexOf(".") != -1) {
|
||||
this.shortId = id.substring(id.indexOf(".") + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("Favorite {id=").append(id).append(", shortId=").append(shortId).append(", name=").append(name)
|
||||
.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.utils;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* Collection of methods to help retrieve HTTP data from a SqueezeServer
|
||||
*
|
||||
* @author Dan Cunningham - Initial contribution
|
||||
* @author Svilen Valkanov - replaced Apache HttpClient with Jetty
|
||||
* @author Mark Hilbush - Add support for LMS authentication
|
||||
* @author Mark Hilbush - Rework exception handling
|
||||
*/
|
||||
public class HttpUtils {
|
||||
private static Logger logger = LoggerFactory.getLogger(HttpUtils.class);
|
||||
|
||||
private static final int TIMEOUT = 5000;
|
||||
private static HttpClient client = new HttpClient();
|
||||
/**
|
||||
* JSON request to get the CLI port from a Squeeze Server
|
||||
*/
|
||||
private static final String JSON_REQ = "{\"params\": [\"\", [\"pref\" ,\"plugin.cli:cliport\",\"?\"]], \"id\": 1, \"method\": \"slim.request\"}";
|
||||
|
||||
/**
|
||||
* Simple logic to perform a post request
|
||||
*
|
||||
* @param url URL to be sent to LMS server
|
||||
* @param postData Data to be sent to LMS server
|
||||
* @return Content received from LMS
|
||||
* @throws SqueezeBoxCommunicationException
|
||||
* @throws SqueezeBoxNotAuthorizedException
|
||||
*/
|
||||
public static String post(String url, String postData)
|
||||
throws SqueezeBoxNotAuthorizedException, SqueezeBoxCommunicationException {
|
||||
if (!client.isStarted()) {
|
||||
try {
|
||||
client.start();
|
||||
} catch (Exception e) {
|
||||
throw new SqueezeBoxCommunicationException("Jetty http client exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ContentResponse response;
|
||||
try {
|
||||
response = client.newRequest(url).method(HttpMethod.POST).content(new StringContentProvider(postData))
|
||||
.timeout(TIMEOUT, TimeUnit.MILLISECONDS).send();
|
||||
} catch (InterruptedException | TimeoutException | ExecutionException e) {
|
||||
throw new SqueezeBoxCommunicationException("Jetty http client exception: " + e.getMessage());
|
||||
}
|
||||
|
||||
int statusCode = response.getStatus();
|
||||
|
||||
if (statusCode == HttpStatus.UNAUTHORIZED_401) {
|
||||
String statusLine = response.getStatus() + " " + response.getReason();
|
||||
logger.error("Received '{}' from squeeze server", statusLine);
|
||||
throw new SqueezeBoxNotAuthorizedException("Unauthorized: " + statusLine);
|
||||
}
|
||||
|
||||
if (statusCode != HttpStatus.OK_200) {
|
||||
String statusLine = response.getStatus() + " " + response.getReason();
|
||||
logger.error("HTTP POST method failed: {}", statusLine);
|
||||
throw new SqueezeBoxCommunicationException("Http post to server failed: " + statusLine);
|
||||
}
|
||||
|
||||
return response.getContentAsString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the command line port (cli) from a SqueezeServer
|
||||
*
|
||||
* @param ip
|
||||
* @param webPort
|
||||
* @return Command Line Interpreter (CLI) port number
|
||||
* @throws SqueezeBoxNotAuthorizedException
|
||||
* @throws SqueezeBoxCommunicationException
|
||||
* @throws NumberFormatException
|
||||
*/
|
||||
public static int getCliPort(String ip, int webPort)
|
||||
throws SqueezeBoxNotAuthorizedException, SqueezeBoxCommunicationException {
|
||||
String url = "http://" + ip + ":" + webPort + "/jsonrpc.js";
|
||||
String json = HttpUtils.post(url, JSON_REQ);
|
||||
logger.trace("Recieved json from server {}", json);
|
||||
JsonElement resp = new JsonParser().parse(json);
|
||||
String cliPort = resp.getAsJsonObject().get("result").getAsJsonObject().get("_p2").getAsString();
|
||||
return Integer.parseInt(cliPort);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.utils;
|
||||
|
||||
/**
|
||||
* Exception thrown when unable to communicate with LMS server.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class SqueezeBoxCommunicationException extends Exception {
|
||||
private static final long serialVersionUID = 1540489268747099161L;
|
||||
|
||||
public SqueezeBoxCommunicationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SqueezeBoxCommunicationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.utils;
|
||||
|
||||
/**
|
||||
* Exception thrown when calling LMS command line interface, and
|
||||
* the LMS is set up to require authentication.
|
||||
*
|
||||
* @author Mark Hilbush - Initial contribution
|
||||
*/
|
||||
public class SqueezeBoxNotAuthorizedException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5190671725971757821L;
|
||||
|
||||
public SqueezeBoxNotAuthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SqueezeBoxNotAuthorizedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.squeezebox.internal.utils;
|
||||
|
||||
/***
|
||||
*
|
||||
* Exception class to indicate a timeout during comminication with
|
||||
* the media server.
|
||||
*
|
||||
* @author Patrik Gfeller - Initial contribution
|
||||
*
|
||||
*/
|
||||
public class SqueezeBoxTimeoutException extends Exception {
|
||||
private static final long serialVersionUID = 4542388088266882905L;
|
||||
|
||||
public SqueezeBoxTimeoutException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="squeezebox" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>SqueezeBox Binding</name>
|
||||
<description>This is the binding for the Logitech Squeeze Server and Players.</description>
|
||||
<author>Dan Cunningham</author>
|
||||
|
||||
<config-description>
|
||||
<parameter name="callbackUrl" type="text">
|
||||
<label>Callback URL</label>
|
||||
<description>URL to use for playing notification sounds, e.g. http://192.168.0.2:8080</description>
|
||||
<required>false</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,307 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="squeezebox"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<bridge-type id="squeezeboxserver">
|
||||
<label>SqueezeBox Server</label>
|
||||
<description>This is a SqueezeBox Server instance.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="favoritesList" typeId="favoritesList"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>ipAddress</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="ipAddress" type="text" required="true">
|
||||
<label>IP or Host Name</label>
|
||||
<description>The IP or host name of the SqueezeServer
|
||||
</description>
|
||||
</parameter>
|
||||
<parameter name="webport" type="integer" required="true" min="1" max="65535">
|
||||
<label>SqueezeServer Web Port</label>
|
||||
<description>Webport interface of the SqueezeServer</description>
|
||||
<default>9000</default>
|
||||
</parameter>
|
||||
<parameter name="cliport" type="integer" required="true" min="1" max="65535">
|
||||
<label>SqueezeServer CLI Port</label>
|
||||
<description>Port of the CLI interface of the SqueezeServer</description>
|
||||
<default>9090</default>
|
||||
</parameter>
|
||||
<parameter name="language" type="text" required="false">
|
||||
<label>Language</label>
|
||||
<description>Language to use when using Google speech</description>
|
||||
<default>en</default>
|
||||
</parameter>
|
||||
<parameter name="userId" type="text" required="false">
|
||||
<label>User Id</label>
|
||||
<description>User ID used to login to Squeeze Server</description>
|
||||
</parameter>
|
||||
<parameter name="password" type="text" required="false">
|
||||
<label>Password</label>
|
||||
<description>Password used to login to Squeeze Server</description>
|
||||
<context>password</context>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</bridge-type>
|
||||
|
||||
<thing-type id="squeezeboxplayer">
|
||||
<supported-bridge-type-refs>
|
||||
<bridge-type-ref id="squeezeboxserver"/>
|
||||
</supported-bridge-type-refs>
|
||||
|
||||
<label>SqueezeBox Player</label>
|
||||
<description>This is a SqueezeBox Player instance</description>
|
||||
|
||||
<channels>
|
||||
<channel id="power" typeId="power"/>
|
||||
<channel id="mute" typeId="mute"/>
|
||||
<channel id="volume" typeId="volume"/>
|
||||
<channel id="stop" typeId="stop"/>
|
||||
<channel id="playPause" typeId="playPause"/>
|
||||
<channel id="next" typeId="next"/>
|
||||
<channel id="prev" typeId="prev"/>
|
||||
<channel id="control" typeId="control"/>
|
||||
<channel id="stream" typeId="stream"/>
|
||||
<channel id="source" typeId="source"/>
|
||||
<channel id="sync" typeId="sync"/>
|
||||
<channel id="unsync" typeId="unsync"/>
|
||||
<channel id="playListIndex" typeId="playListIndex"/>
|
||||
<channel id="currentPlayingTime" typeId="currentPlayingTime"/>
|
||||
<channel id="duration" typeId="duration"/>
|
||||
<channel id="currentPlaylistShuffle" typeId="currentPlaylistShuffle"/>
|
||||
<channel id="currentPlaylistRepeat" typeId="currentPlaylistRepeat"/>
|
||||
<channel id="title" typeId="title"/>
|
||||
<channel id="remotetitle" typeId="remotetitle"/>
|
||||
<channel id="album" typeId="album"/>
|
||||
<channel id="artist" typeId="artist"/>
|
||||
<channel id="year" typeId="year"/>
|
||||
<channel id="genre" typeId="genre"/>
|
||||
<channel id="coverartdata" typeId="coverartdata"/>
|
||||
<channel id="ircode" typeId="ircode"/>
|
||||
<channel id="numberPlaylistTracks" typeId="numberPlaylistTracks"/>
|
||||
<channel id="playFavorite" typeId="playFavorite"/>
|
||||
<channel id="rate" typeId="rate"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="vendor">Logitech</property>
|
||||
<property name="modelId"></property>
|
||||
<property name="name"></property>
|
||||
<property name="uid"></property>
|
||||
<property name="ip"></property>
|
||||
</properties>
|
||||
|
||||
<representation-property>mac</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="mac" type="text" required="true">
|
||||
<label>MAC Address</label>
|
||||
<description>SqueezeBox Players are identified by their MAC address</description>
|
||||
</parameter>
|
||||
<parameter name="notificationTimeout" type="integer" unit="s">
|
||||
<label>Notification Timeout</label>
|
||||
<description>Maximum amount of time in seconds for which the notification will be played</description>
|
||||
<default>20</default>
|
||||
</parameter>
|
||||
<parameter name="notificationVolume" type="integer" min="0" max="100" step="1" unit="%">
|
||||
<label>Notification Sound Volume</label>
|
||||
<description>Volume used for notifications. Leaving blank uses current player volume.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<!-- Favorites -->
|
||||
<channel-type id="favoritesList">
|
||||
<item-type>String</item-type>
|
||||
<label>Favorites List</label>
|
||||
<description>Comma-separated list of favorites of form favoriteId=favoriteName</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
<config-description>
|
||||
<parameter name="quoteList" type="boolean" required="true">
|
||||
<label>Quote Favorites</label>
|
||||
<description>Wrap the right hand side of the favorites in quotes</description>
|
||||
<default>false</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</channel-type>
|
||||
<channel-type id="playFavorite">
|
||||
<item-type>String</item-type>
|
||||
<label>Play a Favorite</label>
|
||||
<description>Play favorite by sending command with favoriteId</description>
|
||||
<state pattern="%s"></state>
|
||||
</channel-type>
|
||||
|
||||
<!-- Commands -->
|
||||
<channel-type id="power" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Power</label>
|
||||
<description>Power on/off your device</description>
|
||||
</channel-type>
|
||||
<channel-type id="mute" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Mute</label>
|
||||
<description>Mute/unmute your device</description>
|
||||
</channel-type>
|
||||
<channel-type id="volume">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Volume</label>
|
||||
<description>Volume of your device</description>
|
||||
<state min="0" max="100" step="1" pattern="%d %%">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="stop" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Stop</label>
|
||||
<description>Stop the current title</description>
|
||||
</channel-type>
|
||||
<channel-type id="playPause" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Play/Pause</label>
|
||||
<description>Plays (on) or pauses (off) the player</description>
|
||||
</channel-type>
|
||||
<channel-type id="pause" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Pause</label>
|
||||
<description>Send a pause command to the player</description>
|
||||
</channel-type>
|
||||
<channel-type id="next" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Next</label>
|
||||
<description>Send a next command to the player</description>
|
||||
</channel-type>
|
||||
<channel-type id="prev" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Previous</label>
|
||||
<description>Send a previous command to the player</description>
|
||||
</channel-type>
|
||||
<channel-type id="control">
|
||||
<item-type>Player</item-type>
|
||||
<label>Control</label>
|
||||
<description>Control the Zone Player, e.g. start/stop/next/previous/ffward/rewind</description>
|
||||
<category>Player</category>
|
||||
</channel-type>
|
||||
<channel-type id="stream" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Stream URL</label>
|
||||
<description>Play the given HTTP or file stream (file:// or http://). </description>
|
||||
</channel-type>
|
||||
<channel-type id="source" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Source</label>
|
||||
<description>Shows the source of the currently playing playlist entry.</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="sync" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Sync Player</label>
|
||||
<description>Add another player to your device for synchronized playback (other player mac address)</description>
|
||||
</channel-type>
|
||||
<channel-type id="unsync" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>UnSync Player</label>
|
||||
<description>Remove this device from synchronization</description>
|
||||
</channel-type>
|
||||
<channel-type id="playListIndex" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Playlist Index</label>
|
||||
<description>Playlist index</description>
|
||||
</channel-type>
|
||||
<channel-type id="currentPlayingTime">
|
||||
<item-type>Number</item-type>
|
||||
<label>Current Playing Time</label>
|
||||
<description>Current Playing Time</description>
|
||||
</channel-type>
|
||||
<channel-type id="duration">
|
||||
<item-type>Number</item-type>
|
||||
<label>Track Duration</label>
|
||||
<description>Duration of Current Track (in seconds)</description>
|
||||
</channel-type>
|
||||
<channel-type id="currentPlaylistShuffle">
|
||||
<item-type>Number</item-type>
|
||||
<label>Shuffle Mode</label>
|
||||
<description>Current playlist shuffle mode</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">No Shuffle</option>
|
||||
<option value="1">Shuffle Songs</option>
|
||||
<option value="2">Shuffle Albums</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="currentPlaylistRepeat">
|
||||
<item-type>Number</item-type>
|
||||
<label>Repeat Mode</label>
|
||||
<description>Current playlist repeat Mode</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">No Repeat</option>
|
||||
<option value="1">Repeat Song</option>
|
||||
<option value="2">Repeat Playlist</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<!-- Squeezebox variables -->
|
||||
<channel-type id="title">
|
||||
<item-type>String</item-type>
|
||||
<label>Title</label>
|
||||
<description>Title of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="remotetitle" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Remote Title</label>
|
||||
<description>Remote Title (Radio) of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="album">
|
||||
<item-type>String</item-type>
|
||||
<label>Album</label>
|
||||
<description>Album name of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="artist">
|
||||
<item-type>String</item-type>
|
||||
<label>Artist</label>
|
||||
<description>Artist name of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="year" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Year</label>
|
||||
<description>Release year of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="genre" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Genre</label>
|
||||
<description>Genre name of the current song</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="coverartdata">
|
||||
<item-type>Image</item-type>
|
||||
<label>Cover Art</label>
|
||||
<description>Image data of cover art of the current song</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="ircode" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>IR Code</label>
|
||||
<description>String of the cached IR code</description>
|
||||
<state readOnly="true" pattern="%s"></state>
|
||||
</channel-type>
|
||||
<channel-type id="numberPlaylistTracks" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Number of Playlist Tracks</label>
|
||||
<description>Number of playlist tracks</description>
|
||||
<state readOnly="true" pattern="%d"></state>
|
||||
</channel-type>
|
||||
<channel-type id="rate" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Like or Unlike Song</label>
|
||||
<description>Likes or unlikes the current song (if the service supports it)</description>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user