added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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