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,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.allplay-${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-allplay" description="AllPlay Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.allplay/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,148 @@
/**
* 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.allplay.internal;
import java.io.IOException;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.openhab.binding.allplay.internal.handler.AllPlayHandler;
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.URLAudioStream;
import org.openhab.core.audio.UnsupportedAudioFormatException;
import org.openhab.core.audio.UnsupportedAudioStreamException;
import org.openhab.core.library.types.PercentType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.kaizencode.tchaikovsky.exception.SpeakerException;
/**
* The {@link AllPlayAudioSink} make AllPlay speakers available as a {@link AudioSink}.
*
* @author Dominic Lerbs - Initial contribution
*/
public class AllPlayAudioSink implements AudioSink {
private final Logger logger = LoggerFactory.getLogger(AllPlayAudioSink.class);
private static final HashSet<AudioFormat> SUPPORTED_FORMATS = new HashSet<>();
private static final HashSet<Class<? extends AudioStream>> SUPPORTED_STREAMS = new HashSet<>();
private final AllPlayHandler handler;
private final AudioHTTPServer audioHTTPServer;
private final String callbackUrl;
static {
SUPPORTED_FORMATS.add(AudioFormat.MP3);
SUPPORTED_FORMATS.add(AudioFormat.WAV);
SUPPORTED_STREAMS.add(AudioStream.class);
}
/**
* @param handler The related {@link AllPlayHandler}
* @param audioHTTPServer The {@link AudioHTTPServer} for serving the stream
* @param callbackUrl The callback URL to stream the audio from
*/
public AllPlayAudioSink(AllPlayHandler handler, AudioHTTPServer audioHTTPServer, String callbackUrl) {
this.handler = handler;
this.audioHTTPServer = audioHTTPServer;
this.callbackUrl = callbackUrl;
}
@Override
public String getId() {
return handler.getThing().getUID().toString();
}
@Override
public String getLabel(Locale locale) {
return handler.getThing().getLabel();
}
@Override
public void process(AudioStream audioStream)
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
try {
String url = convertAudioStreamToUrl(audioStream);
handler.playUrl(url);
} catch (SpeakerException | AllPlayCallbackException e) {
logger.warn("Unable to play audio stream on speaker {}", getId(), e);
}
}
@Override
public Set<AudioFormat> getSupportedFormats() {
return SUPPORTED_FORMATS;
}
@Override
public Set<Class<? extends AudioStream>> getSupportedStreams() {
return SUPPORTED_STREAMS;
}
@Override
public PercentType getVolume() throws IOException {
try {
return handler.getVolume();
} catch (SpeakerException e) {
throw new IOException(e);
}
}
@Override
public void setVolume(PercentType volume) throws IOException {
try {
handler.handleVolumeCommand(volume);
} catch (SpeakerException e) {
throw new IOException(e);
}
}
/**
* Converts the given {@link AudioStream} into an URL which can be used for streaming.
*
* @param audioStream The incoming {@link AudioStream}
* @return The URL to use for streaming
* @throws AllPlayCallbackException Exception if the URL cannot be created
*/
private String convertAudioStreamToUrl(AudioStream audioStream) throws AllPlayCallbackException {
if (audioStream instanceof URLAudioStream) {
// it is an external URL, the speaker can access it itself and play it
return ((URLAudioStream) audioStream).getURL();
} else {
return createUrlForLocalHttpServer(audioStream);
}
}
private String createUrlForLocalHttpServer(AudioStream audioStream) throws AllPlayCallbackException {
if (callbackUrl != null) {
String relativeUrl = audioHTTPServer.serve(audioStream);
return callbackUrl + relativeUrl;
} else {
throw new AllPlayCallbackException("Unable to play audio stream as callback URL is not set");
}
}
@SuppressWarnings("serial")
private class AllPlayCallbackException extends Exception {
public AllPlayCallbackException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,60 @@
/**
* 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.allplay.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link AllPlayBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Dominic Lerbs - Initial contribution
*/
@NonNullByDefault
public class AllPlayBindingConstants {
public static final String BINDING_ID = "allplay";
// List of all Thing Type UIDs
public static final ThingTypeUID SPEAKER_THING_TYPE = new ThingTypeUID(BINDING_ID, "speaker");
// List of all Channel ids
public static final String CLEAR_ZONE = "clearzone";
public static final String CONTROL = "control";
public static final String CURRENT_ALBUM = "currentalbum";
public static final String CURRENT_ARTIST = "currentartist";
public static final String CURRENT_DURATION = "currentduration";
public static final String CURRENT_GENRE = "currentgenre";
public static final String CURRENT_TITLE = "currenttitle";
public static final String CURRENT_URL = "currenturl";
public static final String CURRENT_USER_DATA = "currentuserdata";
public static final String INPUT = "input";
public static final String LOOP_MODE = "loopmode";
public static final String MUTE = "mute";
public static final String PLAY_STATE = "playstate";
public static final String SHUFFLE_MODE = "shufflemode";
public static final String STOP = "stop";
public static final String STREAM = "stream";
public static final String COVER_ART = "coverart";
public static final String COVER_ART_URL = "coverarturl";
public static final String VOLUME = "volume";
public static final String VOLUME_CONTROL = "volumecontrol";
public static final String ZONE_ID = "zoneid";
public static final String ZONE_MEMBERS = "zonemembers";
// Config properties
public static final String DEVICE_ID = "deviceId";
public static final String DEVICE_NAME = "deviceName";
public static final String VOLUME_STEP_SIZE = "volumeStepSize";
}

View File

@@ -0,0 +1,87 @@
/**
* 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.allplay.internal;
import java.util.Dictionary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AllPlay binding properties.
*
* @author Dominic Lerbs - Initial contribution
*/
public class AllPlayBindingProperties {
private final Logger logger = LoggerFactory.getLogger(AllPlayBindingProperties.class);
private final int rewindSkipTimeInSec;
private final int fastForwardSkipTimeInSec;
private final String callbackUrl;
private final String zoneMemberSeparator;
private static final String REWIND_SKIP_TIME_PROPERTY = "rewindSkipTimeInSec";
private static final int REWIND_SKIP_TIME_DEFAULT_VALUE = 10;
private static final String FAST_FORWARD_SKIP_TIME_PROPERTY = "fastForwardSkipTimeInSec";
private static final int FAST_FORWARD_SKIP_TIME_DEFAULT_VALUE = 10;
private static final String CALLBACK_URL = "callbackUrl";
private static final String ZONE_MEMBER_SEPARATOR_PROPERTY = "zoneMemberSeparator";
private static final String ZONE_MEMBER_SEPARATOR_DEFAULT_VALUE = ",";
public AllPlayBindingProperties(Dictionary<String, Object> properties) {
rewindSkipTimeInSec = getIntegerProperty(properties, REWIND_SKIP_TIME_PROPERTY, REWIND_SKIP_TIME_DEFAULT_VALUE);
fastForwardSkipTimeInSec = getIntegerProperty(properties, FAST_FORWARD_SKIP_TIME_PROPERTY,
FAST_FORWARD_SKIP_TIME_DEFAULT_VALUE);
callbackUrl = (String) properties.get(CALLBACK_URL);
zoneMemberSeparator = getStringProperty(properties, ZONE_MEMBER_SEPARATOR_PROPERTY,
ZONE_MEMBER_SEPARATOR_DEFAULT_VALUE);
}
public int getRewindSkipTimeInSec() {
return rewindSkipTimeInSec;
}
public int getFastForwardSkipTimeInSec() {
return fastForwardSkipTimeInSec;
}
public String getCallbackUrl() {
return callbackUrl;
}
private int getIntegerProperty(Dictionary<String, Object> properties, String propertyKey, int defaultValue) {
Object configValue = properties.get(propertyKey);
int value = defaultValue;
if (configValue instanceof String) {
try {
value = Integer.parseInt((String) configValue);
} catch (NumberFormatException e) {
logger.warn("Unable to convert value {} for config property {} to integer. Using default value.",
configValue, propertyKey);
}
} else if (configValue instanceof Integer) {
value = (int) configValue;
}
return value;
}
public String getZoneMemberSeparator() {
return zoneMemberSeparator;
}
private String getStringProperty(Dictionary<String, Object> properties, String propertyKey, String defaultValue) {
String value = (String) properties.get(propertyKey);
return value != null ? value : defaultValue;
}
}

View File

@@ -0,0 +1,161 @@
/**
* 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.allplay.internal;
import static org.openhab.binding.allplay.internal.AllPlayBindingConstants.SPEAKER_THING_TYPE;
import java.util.Collections;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.openhab.binding.allplay.internal.handler.AllPlayHandler;
import org.openhab.core.audio.AudioHTTPServer;
import org.openhab.core.audio.AudioSink;
import org.openhab.core.net.HttpServiceUtil;
import org.openhab.core.net.NetworkAddressService;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingRegistry;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.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;
import de.kaizencode.tchaikovsky.AllPlay;
import de.kaizencode.tchaikovsky.exception.AllPlayException;
/**
* The {@link AllPlayHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Dominic Lerbs - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.allplay")
public class AllPlayHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(AllPlayHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(SPEAKER_THING_TYPE);
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
private AllPlay allPlay;
private AllPlayBindingProperties bindingProperties;
// Bindings should not use the ThingRegistry! See https://github.com/openhab/openhab-addons/pull/6080 and
// https://github.com/eclipse/smarthome/issues/5182
private final ThingRegistry thingRegistry;
private final AudioHTTPServer audioHTTPServer;
private final NetworkAddressService networkAddressService;
private String callbackUrl;
@Activate
public AllPlayHandlerFactory(final @Reference ThingRegistry thingRegistry,
final @Reference AudioHTTPServer audioHTTPServer,
final @Reference NetworkAddressService networkAddressService) {
this.thingRegistry = thingRegistry;
this.audioHTTPServer = audioHTTPServer;
this.networkAddressService = networkAddressService;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(SPEAKER_THING_TYPE)) {
logger.debug("Creating AllPlayHandler for thing {}", thing.getUID());
AllPlayHandler handler = new AllPlayHandler(thingRegistry, thing, allPlay, bindingProperties);
registerAudioSink(thing, handler);
return handler;
}
return null;
}
private void registerAudioSink(Thing thing, AllPlayHandler handler) {
AllPlayAudioSink audioSink = new AllPlayAudioSink(handler, audioHTTPServer, callbackUrl);
@SuppressWarnings("unchecked")
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
.registerService(AudioSink.class.getName(), audioSink, new Hashtable<>());
audioSinkRegistrations.put(thing.getUID().toString(), reg);
}
@Override
public void unregisterHandler(Thing thing) {
super.unregisterHandler(thing);
unregisterAudioSink(thing);
}
private void unregisterAudioSink(Thing thing) {
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.get(thing.getUID().toString());
if (reg != null) {
reg.unregister();
}
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
logger.debug("Activating AllPlayHandlerFactory");
allPlay = new AllPlay("openHAB2");
try {
logger.debug("Connecting to AllPlay");
allPlay.connect();
} catch (AllPlayException e) {
logger.error("Cannot initialize AllPlay", e);
}
Dictionary<String, Object> properties = componentContext.getProperties();
bindingProperties = new AllPlayBindingProperties(properties);
callbackUrl = assembleCallbackUrl();
}
@Override
protected void deactivate(ComponentContext componentContext) {
logger.debug("Deactivating AllPlayHandlerFactory");
allPlay.disconnect();
allPlay = null;
super.deactivate(componentContext);
}
private String assembleCallbackUrl() {
String callbackUrl = bindingProperties.getCallbackUrl();
if (callbackUrl == null) {
String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
}
// we do not use SSL as it can cause certificate validation issues.
final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
if (port == -1) {
logger.warn("Cannot find port of the http service.");
return null;
}
callbackUrl = "http://" + ipAddress + ":" + port;
}
return callbackUrl;
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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.allplay.internal.discovery;
import static org.openhab.binding.allplay.internal.AllPlayBindingConstants.SPEAKER_THING_TYPE;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.openhab.binding.allplay.internal.AllPlayBindingConstants;
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.config.discovery.DiscoveryService;
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;
import de.kaizencode.tchaikovsky.AllPlay;
import de.kaizencode.tchaikovsky.exception.AllPlayException;
import de.kaizencode.tchaikovsky.listener.SpeakerAnnouncedListener;
import de.kaizencode.tchaikovsky.speaker.Speaker;
/**
* Discovery service to scan for AllPlay devices.
*
* @author Dominic Lerbs - Initial contribution
*/
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.allplay")
public class AllPlaySpeakerDiscoveryService extends AbstractDiscoveryService implements SpeakerAnnouncedListener {
private final Logger logger = LoggerFactory.getLogger(AllPlaySpeakerDiscoveryService.class);
private static final int DISCOVERY_TIMEOUT = 30;
private static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Collections.singleton(SPEAKER_THING_TYPE);
private AllPlay allPlay;
public AllPlaySpeakerDiscoveryService() {
super(DISCOVERABLE_THING_TYPES_UIDS, DISCOVERY_TIMEOUT);
}
@Override
protected void startScan() {
logger.debug("Starting scan for AllPlay devices");
try {
allPlay = new AllPlay("openHAB2-discovery");
allPlay.addSpeakerAnnouncedListener(this);
allPlay.connect();
allPlay.discoverSpeakers();
} catch (AllPlayException e) {
logger.warn("Error while scanning for AllPlay devices", e);
}
}
@Override
protected void stopScan() {
logger.debug("Stopping scan for AllPlay devices");
if (allPlay != null) {
allPlay.removeSpeakerAnnouncedListener(this);
allPlay.disconnect();
}
super.stopScan();
}
@Override
protected void startBackgroundDiscovery() {
logger.trace("Starting background scan for AllPlay devices");
startScan();
}
@Override
protected void stopBackgroundDiscovery() {
logger.trace("Stopping background scan for AllPlay devices");
stopScan();
}
@Override
public void deactivate() {
removeOlderResults(getTimestampOfLastScan());
}
@Override
public void onSpeakerAnnounced(Speaker speaker) {
logger.debug("Speaker {} found by discovery service", speaker);
ThingUID thingUID = new ThingUID(AllPlayBindingConstants.SPEAKER_THING_TYPE, speaker.getId());
Map<String, Object> properties = new HashMap<>();
properties.put(AllPlayBindingConstants.DEVICE_ID, speaker.getId());
properties.put(AllPlayBindingConstants.DEVICE_NAME, speaker.getName());
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withRepresentationProperty(AllPlayBindingConstants.DEVICE_ID).withLabel(speaker.getName()).build();
thingDiscovered(discoveryResult);
}
}

View File

@@ -0,0 +1,617 @@
/**
* 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.allplay.internal.handler;
import static org.openhab.binding.allplay.internal.AllPlayBindingConstants.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.openhab.binding.allplay.internal.AllPlayBindingConstants;
import org.openhab.binding.allplay.internal.AllPlayBindingProperties;
import org.openhab.core.common.ThreadPoolManager;
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.ThingRegistry;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import de.kaizencode.tchaikovsky.AllPlay;
import de.kaizencode.tchaikovsky.exception.AllPlayException;
import de.kaizencode.tchaikovsky.exception.ConnectionException;
import de.kaizencode.tchaikovsky.exception.DiscoveryException;
import de.kaizencode.tchaikovsky.exception.SpeakerException;
import de.kaizencode.tchaikovsky.listener.SpeakerAnnouncedListener;
import de.kaizencode.tchaikovsky.listener.SpeakerChangedListener;
import de.kaizencode.tchaikovsky.listener.SpeakerConnectionListener;
import de.kaizencode.tchaikovsky.speaker.PlayState;
import de.kaizencode.tchaikovsky.speaker.PlayState.State;
import de.kaizencode.tchaikovsky.speaker.PlaylistItem;
import de.kaizencode.tchaikovsky.speaker.Speaker;
import de.kaizencode.tchaikovsky.speaker.Speaker.LoopMode;
import de.kaizencode.tchaikovsky.speaker.Speaker.ShuffleMode;
import de.kaizencode.tchaikovsky.speaker.VolumeRange;
import de.kaizencode.tchaikovsky.speaker.ZoneItem;
/**
* The {@link AllPlayHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Dominic Lerbs - Initial contribution
*/
public class AllPlayHandler extends BaseThingHandler
implements SpeakerChangedListener, SpeakerAnnouncedListener, SpeakerConnectionListener {
private final Logger logger = LoggerFactory.getLogger(AllPlayHandler.class);
private final ThingRegistry localThingRegistry;
private final AllPlay allPlay;
private final AllPlayBindingProperties bindingProperties;
private Speaker speaker;
private VolumeRange volumeRange;
private static final String ALLPLAY_THREADPOOL_NAME = "allplayHandler";
private ScheduledFuture<?> reconnectionJob;
private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(ALLPLAY_THREADPOOL_NAME);
public AllPlayHandler(ThingRegistry thingRegistry, Thing thing, AllPlay allPlay,
AllPlayBindingProperties properties) {
super(thing);
this.localThingRegistry = thingRegistry;
this.allPlay = allPlay;
this.bindingProperties = properties;
}
@Override
public void initialize() {
logger.debug("Initializing AllPlay handler for speaker {}", getDeviceId());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Waiting for speaker to be discovered");
try {
allPlay.addSpeakerAnnouncedListener(this);
discoverSpeaker();
} catch (DiscoveryException e) {
logger.error("Unable to discover speaker {}", getDeviceId(), e);
}
}
/**
* Tries to discover the speaker which is associated with this thing.
*/
public void discoverSpeaker() {
try {
logger.debug("Starting discovery for speaker {}", getDeviceId());
allPlay.discoverSpeaker(getDeviceId());
} catch (DiscoveryException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unable to discover speaker: " + e.getMessage());
logger.error("Unable to discover speaker {}", getDeviceId(), e);
}
}
@Override
public void onSpeakerAnnounced(Speaker speaker) {
logger.debug("Speaker announcement received for speaker {}. Own id is {}", speaker, getDeviceId());
if (isHandledSpeaker(speaker)) {
logger.debug("Speaker announcement received for handled speaker {}", speaker);
if (this.speaker != null) {
// Make sure to disconnect first in case the speaker is re-announced
disconnectFromSpeaker(this.speaker);
}
this.speaker = speaker;
cancelReconnectionJob();
try {
connectToSpeaker();
} catch (AllPlayException e) {
logger.debug("Connection error", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Error while communicating with speaker: " + e.getMessage());
scheduleReconnectionJob(speaker);
}
}
}
private void connectToSpeaker() throws ConnectionException {
if (speaker != null) {
logger.debug("Connecting to speaker {}", speaker);
speaker.addSpeakerChangedListener(this);
speaker.addSpeakerConnectionListener(this);
speaker.connect();
logger.debug("Connected to speaker {}", speaker);
updateStatus(ThingStatus.ONLINE);
try {
initSpeakerState();
} catch (SpeakerException e) {
logger.error("Unable to init speaker state", e);
}
} else {
logger.error("Speaker {} not discovered yet, cannot connect", getDeviceId());
}
}
private void initSpeakerState() throws SpeakerException {
cacheVolumeRange();
onMuteChanged(speaker.volume().isMute());
onLoopModeChanged(speaker.getLoopMode());
onShuffleModeChanged(speaker.getShuffleMode());
onPlayStateChanged(speaker.getPlayState());
onVolumeChanged(speaker.volume().getVolume());
onVolumeControlChanged(speaker.volume().isControlEnabled());
}
/**
* Cache the volume range as it will not change for the speaker.
*/
private void cacheVolumeRange() throws SpeakerException {
volumeRange = speaker.volume().getVolumeRange();
}
@Override
public void onConnectionLost(String wellKnownName, int alljoynReasonCode) {
if (isHandledSpeaker(wellKnownName)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Lost connection to speaker");
speaker.removeSpeakerConnectionListener(this);
speaker.removeSpeakerChangedListener(this);
scheduleReconnectionJob(speaker);
}
}
@Override
public void dispose() {
allPlay.removeSpeakerAnnouncedListener(this);
if (speaker != null) {
disconnectFromSpeaker(speaker);
}
super.dispose();
}
private void disconnectFromSpeaker(Speaker speaker) {
logger.debug("Disconnecting from speaker {}", speaker);
speaker.removeSpeakerChangedListener(this);
speaker.removeSpeakerConnectionListener(this);
cancelReconnectionJob();
if (speaker.isConnected()) {
speaker.disconnect();
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Channel {} triggered with command {}", channelUID.getId(), command);
if (isSpeakerReady()) {
try {
if (command instanceof RefreshType) {
handleRefreshCommand(channelUID.getId());
} else {
handleSpeakerCommand(channelUID.getId(), command);
}
} catch (SpeakerException e) {
logger.error("Unable to execute command {} on channel {}", command, channelUID.getId(), e);
}
}
}
private void handleSpeakerCommand(String channelId, Command command) throws SpeakerException {
switch (channelId) {
case CLEAR_ZONE:
if (OnOffType.ON.equals(command)) {
speaker.zoneManager().releaseZone();
}
break;
case CONTROL:
handleControlCommand(command);
break;
case INPUT:
speaker.input().setInput(command.toString());
break;
case LOOP_MODE:
speaker.setLoopMode(LoopMode.parse(command.toString()));
break;
case MUTE:
speaker.volume().mute(OnOffType.ON.equals(command));
break;
case STOP:
speaker.stop();
break;
case SHUFFLE_MODE:
handleShuffleModeCommand(command);
break;
case STREAM:
logger.debug("Starting to stream URL: {}", command.toString());
speaker.playItem(command.toString());
break;
case VOLUME:
handleVolumeCommand(command);
break;
case ZONE_MEMBERS:
handleZoneMembersCommand(command);
break;
default:
logger.warn("Unable to handle command {} on unknown channel {}", command, channelId);
}
}
private void handleRefreshCommand(String channelId) throws SpeakerException {
switch (channelId) {
case CURRENT_ARTIST:
case CURRENT_ALBUM:
case CURRENT_DURATION:
case CURRENT_GENRE:
case CURRENT_TITLE:
case CURRENT_URL:
updatePlaylistItemsState(speaker.getPlayState().getPlaylistItems());
break;
case CONTROL:
updatePlayState(speaker.getPlayState());
break;
case INPUT:
onInputChanged(speaker.input().getActiveInput());
break;
case LOOP_MODE:
onLoopModeChanged(speaker.getLoopMode());
break;
case MUTE:
onMuteChanged(speaker.volume().isMute());
break;
case SHUFFLE_MODE:
onShuffleModeChanged(speaker.getShuffleMode());
break;
case VOLUME:
onVolumeChanged(speaker.volume().getVolume());
onVolumeControlChanged(speaker.volume().isControlEnabled());
break;
case ZONE_ID:
updateState(ZONE_ID, new StringType(speaker.getPlayerInfo().getZoneInfo().getZoneId()));
break;
default:
logger.debug("REFRESH command not implemented on channel {}", channelId);
}
}
private void handleControlCommand(Command command) throws SpeakerException {
if (command instanceof PlayPauseType) {
if (command == PlayPauseType.PLAY) {
speaker.resume();
} else if (command == PlayPauseType.PAUSE) {
speaker.pause();
}
} else if (command instanceof NextPreviousType) {
if (command == NextPreviousType.NEXT) {
speaker.next();
} else if (command == NextPreviousType.PREVIOUS) {
speaker.previous();
}
} else if (command instanceof RewindFastforwardType) {
if (command == RewindFastforwardType.FASTFORWARD) {
changeTrackPosition(bindingProperties.getFastForwardSkipTimeInSec() * 1000);
} else if (command == RewindFastforwardType.REWIND) {
changeTrackPosition(-bindingProperties.getRewindSkipTimeInSec() * 1000);
}
} else {
logger.warn("Unknown control command: {}", command);
}
}
/**
* Changes the position in the current track.
*
* @param positionOffsetInMs The offset to adjust the current position. Can be negative or positive.
* @throws SpeakerException Exception if the position could not be changed
*/
private void changeTrackPosition(long positionOffsetInMs) throws SpeakerException {
long currentPosition = speaker.getPlayState().getPositionInMs();
logger.debug("Jumping from old track position {} ms to new position {} ms", currentPosition,
currentPosition + positionOffsetInMs);
speaker.setPosition(currentPosition + positionOffsetInMs);
}
/**
* Uses the given {@link Command} to change the volume of the speaker.
*
* @param command The {@link Command} with the new volume
* @throws SpeakerException Exception if the volume change failed
*/
public void handleVolumeCommand(Command command) throws SpeakerException {
if (command instanceof PercentType) {
speaker.volume().setVolume(convertPercentToAbsoluteVolume((PercentType) command));
} else if (command instanceof IncreaseDecreaseType) {
int stepSize = (command == IncreaseDecreaseType.DECREASE ? -getVolumeStepSize() : getVolumeStepSize());
speaker.volume().adjustVolume(stepSize);
}
}
private void handleShuffleModeCommand(Command command) throws SpeakerException {
if (OnOffType.ON.equals(command)) {
speaker.setShuffleMode(ShuffleMode.SHUFFLE);
} else if (OnOffType.OFF.equals(command)) {
speaker.setShuffleMode(ShuffleMode.LINEAR);
}
}
private void handleZoneMembersCommand(Command command) throws SpeakerException {
String[] memberNames = command.toString().split(bindingProperties.getZoneMemberSeparator());
logger.debug("{}: Creating new zone with members {}", speaker, String.join(", ", memberNames));
List<String> memberIds = new ArrayList<>();
for (String memberName : memberNames) {
memberIds.add(getHandlerIdByLabel(memberName.trim()));
}
createZoneInNewThread(memberIds);
}
private void createZoneInNewThread(List<String> memberIds) {
scheduler.execute(() -> {
try {
// This call blocks up to 10 seconds if one of the members is unreachable,
// therefore it is executed in a new thread
ZoneItem zone = speaker.zoneManager().createZone(memberIds);
logger.debug("{}: Zone {} with member ids {} has been created", speaker, zone.getZoneId(),
String.join(", ", zone.getSlaves().keySet()));
} catch (SpeakerException e) {
logger.warn("{}: Cannot create zone", speaker, e);
}
});
}
@Override
public void onPlayStateChanged(PlayState playState) {
updatePlayState(playState);
updatePlaylistItemsState(playState.getPlaylistItems());
}
@Override
public void onPlaylistChanged() {
logger.debug("{}: Playlist changed: No action", speaker.getName());
}
@Override
public void onLoopModeChanged(LoopMode loopMode) {
logger.debug("{}: LoopMode changed to {}", speaker.getName(), loopMode);
updateState(LOOP_MODE, new StringType(loopMode.toString()));
}
@Override
public void onShuffleModeChanged(ShuffleMode shuffleMode) {
logger.debug("{}: ShuffleMode changed to {}", speaker.getName(), shuffleMode);
OnOffType shuffleOnOff = (shuffleMode == ShuffleMode.SHUFFLE) ? OnOffType.ON : OnOffType.OFF;
updateState(SHUFFLE_MODE, shuffleOnOff);
}
@Override
public void onMuteChanged(boolean mute) {
logger.debug("{}: Mute changed to {}", speaker.getName(), mute);
updateState(MUTE, mute ? OnOffType.ON : OnOffType.OFF);
}
@Override
public void onVolumeChanged(int volume) {
logger.debug("{}: Volume changed to {}", speaker.getName(), volume);
try {
updateState(VOLUME, convertAbsoluteVolumeToPercent(volume));
} catch (SpeakerException e) {
logger.warn("Cannot convert new volume to percent", e);
}
}
@Override
public void onVolumeControlChanged(boolean enabled) {
updateState(VOLUME_CONTROL, enabled ? OnOffType.ON : OnOffType.OFF);
}
@Override
public void onZoneChanged(String zoneId, int timestamp, Map<String, Integer> slaves) {
logger.debug("{}: Zone changed to {}", speaker.getName(), zoneId);
updateState(ZONE_ID, new StringType(zoneId));
}
@Override
public void onInputChanged(String input) {
logger.debug("{}: Input changed to {}", speaker.getName(), input);
updateState(INPUT, new StringType(input));
}
private void updatePlayState(PlayState playState) {
logger.debug("{}: PlayState changed to {}", speaker.getName(), playState);
updateState(PLAY_STATE, new StringType(playState.getState().toString()));
if (playState.getState() == State.PLAYING) {
updateState(CONTROL, PlayPauseType.PLAY);
} else {
updateState(CONTROL, PlayPauseType.PAUSE);
}
}
private void updatePlaylistItemsState(List<PlaylistItem> items) {
if (!items.isEmpty()) {
PlaylistItem currentItem = items.iterator().next();
updateCurrentItemState(currentItem);
} else {
updateState(CURRENT_ARTIST, UnDefType.NULL);
updateState(CURRENT_ALBUM, UnDefType.NULL);
updateState(CURRENT_TITLE, UnDefType.NULL);
updateState(CURRENT_GENRE, UnDefType.NULL);
updateState(CURRENT_URL, UnDefType.NULL);
updateState(COVER_ART_URL, UnDefType.NULL);
updateState(COVER_ART, UnDefType.NULL);
}
}
private void updateCurrentItemState(PlaylistItem currentItem) {
logger.debug("{}: PlaylistItem changed to {}", speaker.getName(), currentItem);
updateState(CURRENT_ARTIST, new StringType(currentItem.getArtist()));
updateState(CURRENT_ALBUM, new StringType(currentItem.getAlbum()));
updateState(CURRENT_TITLE, new StringType(currentItem.getTitle()));
updateState(CURRENT_GENRE, new StringType(currentItem.getGenre()));
updateDuration(currentItem.getDurationInMs());
updateState(CURRENT_URL, new StringType(currentItem.getUrl()));
updateCoverArtState(currentItem.getThumbnailUrl());
try {
updateState(CURRENT_USER_DATA, new StringType(String.valueOf(currentItem.getUserData())));
} catch (SpeakerException e) {
logger.warn("Unable to update current user data: {}", e.getMessage(), e);
}
logger.debug("MediaType: {}", currentItem.getMediaType());
}
private void updateDuration(long durationInMs) {
DecimalType duration = new DecimalType(durationInMs / 1000);
duration.format("%d s");
updateState(CURRENT_DURATION, duration);
}
private void updateCoverArtState(String coverArtUrl) {
try {
logger.debug("{}: Cover art URL changed to {}", speaker.getName(), coverArtUrl);
updateState(COVER_ART_URL, new StringType(coverArtUrl));
if (!coverArtUrl.isEmpty()) {
byte[] bytes = getRawDataFromUrl(coverArtUrl);
String contentType = HttpUtil.guessContentTypeFromData(bytes);
updateState(COVER_ART, new RawType(bytes,
contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType));
} else {
updateState(COVER_ART, UnDefType.NULL);
}
} catch (Exception e) {
logger.warn("Error getting cover art", e);
}
}
/**
* Starts streaming the audio at the given URL.
*
* @param url The URL to stream
* @throws SpeakerException Exception if the URL could not be streamed
*/
public void playUrl(String url) throws SpeakerException {
if (isSpeakerReady()) {
speaker.playItem(url);
} else {
throw new SpeakerException(
"Cannot play audio stream, speaker " + speaker + " is not discovered/connected!");
}
}
/**
* @return The current volume of the speaker
* @throws SpeakerException Exception if the volume could not be retrieved
*/
public PercentType getVolume() throws SpeakerException {
if (isSpeakerReady()) {
return convertAbsoluteVolumeToPercent(speaker.volume().getVolume());
} else {
throw new SpeakerException("Cannot get volume, speaker " + speaker + " is not discovered/connected!");
}
}
private byte[] getRawDataFromUrl(String urlString) throws Exception {
URL url = new URL(urlString);
URLConnection connection = url.openConnection();
return IOUtils.toByteArray(connection.getInputStream());
}
private int convertPercentToAbsoluteVolume(PercentType percentVolume) throws SpeakerException {
int range = volumeRange.getMax() - volumeRange.getMin();
int volume = (percentVolume.shortValue() * range) / 100;
logger.debug("Volume {}% has been converted to absolute volume {}", percentVolume.intValue(), volume);
return volume;
}
private PercentType convertAbsoluteVolumeToPercent(int volume) throws SpeakerException {
int range = volumeRange.getMax() - volumeRange.getMin();
int percentVolume = 0;
if (range > 0) {
percentVolume = (volume * 100) / range;
}
logger.debug("Absolute volume {} has been converted to volume {}%", volume, percentVolume);
return new PercentType(percentVolume);
}
private boolean isSpeakerReady() {
if (speaker == null || !speaker.isConnected()) {
logger.warn("Cannot execute command, speaker {} is not discovered/connected!", speaker);
return false;
}
return true;
}
/**
* @param speaker The {@link Speaker} to check
* @return True if the {@link Speaker} is managed by this handler, else false
*/
private boolean isHandledSpeaker(Speaker speaker) {
return speaker.getId().equals(getDeviceId());
}
private boolean isHandledSpeaker(String wellKnownName) {
return wellKnownName.equals(speaker.details().getWellKnownName());
}
private String getDeviceId() {
return (String) getConfig().get(AllPlayBindingConstants.DEVICE_ID);
}
private Integer getVolumeStepSize() {
return (Integer) getConfig().get(AllPlayBindingConstants.VOLUME_STEP_SIZE);
}
/**
* Schedules a reconnection job.
*/
private void scheduleReconnectionJob(final Speaker speaker) {
logger.debug("Scheduling job to rediscover to speaker {}", speaker);
// TODO: Check if it makes sense to repeat the discovery every x minutes or if the AllJoyn library is able to
// handle re-discovery in _all_ cases.
cancelReconnectionJob();
reconnectionJob = scheduler.scheduleWithFixedDelay(this::discoverSpeaker, 5, 600, TimeUnit.SECONDS);
}
/**
* Cancels a scheduled reconnection job.
*/
private void cancelReconnectionJob() {
if (reconnectionJob != null) {
reconnectionJob.cancel(true);
}
}
private String getHandlerIdByLabel(String thingLabel) throws IllegalStateException {
for (Thing thing : localThingRegistry.getAll()) {
if (thingLabel.equals(thing.getLabel())) {
return thing.getUID().getId();
}
}
throw new IllegalStateException("Could not find thing with label " + thingLabel);
}
}

View File

@@ -0,0 +1,21 @@
/**
* 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
*/
@org.osgi.annotation.bundle.Header(name = org.osgi.framework.Constants.BUNDLE_NATIVECODE, value = "lib/x86-64/win/alljoyn_java.dll;processor=amd64;osname=win32;osname=Windows8;osname=Windows 8;osname=Win8;osname=Windows10;osname=Windows 10;osname=Win10,lib/x86/win/alljoyn_java.dll;processor=x86;osname=win32;osname=Windows8;osname=Windows 8;osname=Win8;osname=Windows10;osname=Windows 10;osname=Win10,lib/x86/linux/liballjoyn_java.so;processor=x86;osname=linux,lib/x86-64/linux/liballjoyn_java.so;processor=amd64;osname=linux,lib/arm/linux/liballjoyn_java.so;processor=arm;osname=linux,*")
package org.openhab.binding.allplay;
/**
* Additional information for AllPlay package
*
* @author Jan N. Klug - Initial contribution
*
*/

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="allplay" 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>AllPlay Binding</name>
<description>The AllPlay binding integrates devices compatible with Qualcomm AllPlay.</description>
<author>Dominic Lerbs</author>
<config-description>
<parameter name="rewindSkipTimeInSec" type="integer" unit="s">
<label>Rewind Skip Time</label>
<description>Seconds to jump backwards if the rewind command is executed</description>
<default>10</default>
<unitLabel>s</unitLabel>
</parameter>
<parameter name="fastForwardSkipTimeInSec" type="integer" unit="s">
<label>Fast Forward Skip Time</label>
<description>Seconds to jump forward if the fastforward command is executed</description>
<default>10</default>
<unitLabel>s</unitLabel>
</parameter>
<parameter name="callbackUrl" type="text">
<label>Callback URL</label>
<description>URL to use for playing audio streams, e.g. http://192.168.0.2:8080</description>
<required>false</required>
</parameter>
<parameter name="zoneMemberSeparator" type="text">
<label>Zone Member Separator</label>
<description>Separator which is used when sending multiple zone members to channel 'zonemembers'</description>
<default>,</default>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="allplay"
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">
<!-- Thing Type -->
<thing-type id="speaker">
<label>AllPlay Speaker</label>
<channels>
<channel id="clearzone" typeId="clearzone"/>
<channel id="control" typeId="control"/>
<channel id="coverart" typeId="coverart"/>
<channel id="coverarturl" typeId="coverarturl"/>
<channel id="currentalbum" typeId="currentalbum"/>
<channel id="currentartist" typeId="currentartist"/>
<channel id="currentduration" typeId="currentduration"/>
<channel id="currentgenre" typeId="currentgenre"/>
<channel id="currenttitle" typeId="currenttitle"/>
<channel id="currenturl" typeId="currenturl"/>
<channel id="currentuserdata" typeId="currentuserdata"/>
<channel id="input" typeId="input"/>
<channel id="loopmode" typeId="loopmode"/>
<channel id="mute" typeId="mute"/>
<channel id="playstate" typeId="playstate"/>
<channel id="shufflemode" typeId="shufflemode"/>
<channel id="stop" typeId="stop"/>
<channel id="stream" typeId="stream"/>
<channel id="volume" typeId="volume"/>
<channel id="volumecontrol" typeId="volumecontrol"/>
<channel id="zoneid" typeId="zoneid"/>
<channel id="zonemembers" typeId="zonemembers"/>
</channels>
<config-description>
<parameter name="deviceId" type="text">
<label>Device ID</label>
<description>The device identifier identifies one certain speaker.</description>
<required>true</required>
</parameter>
<parameter name="deviceName" type="text">
<label>Device Name</label>
<description>The device name of the speaker.</description>
<required>false</required>
</parameter>
<parameter name="volumeStepSize" type="integer">
<label>Volume Step Size</label>
<description>Step size to use if the volume is changed using the increase/decrease command.</description>
<default>1</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<!-- Channel Type -->
<channel-type id="clearzone" advanced="true">
<item-type>Switch</item-type>
<label>Clear Zone</label>
<description>Remove the current speaker from the zone</description>
</channel-type>
<channel-type id="control">
<item-type>Player</item-type>
<label>Control</label>
<description>Control the AllPlay speaker, e.g. start/pause/next/previous/ffward/rewind</description>
<category>Player</category>
</channel-type>
<channel-type id="coverart">
<item-type>Image</item-type>
<label>Cover Art</label>
<description>Cover art image of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="coverarturl" advanced="true">
<item-type>String</item-type>
<label>Cover Art URL</label>
<description>Cover art URL of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currentalbum">
<item-type>String</item-type>
<label>Current Album</label>
<description>Album of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currentartist">
<item-type>String</item-type>
<label>Current Artist</label>
<description>Artist of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currentduration" advanced="true">
<item-type>Number</item-type>
<label>Current Duration</label>
<description>Duration in seconds of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currentgenre" advanced="true">
<item-type>String</item-type>
<label>Current Genre</label>
<description>Genre of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currenttitle">
<item-type>String</item-type>
<label>Current Title</label>
<description>Title of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currentuserdata" advanced="true">
<item-type>String</item-type>
<label>Current User Data</label>
<description>Custom user data (e.g. name of radio station) of the track currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="currenturl" advanced="true">
<item-type>String</item-type>
<label>Current URL</label>
<description>URL of the track or radio station currently playing</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="input" advanced="true">
<item-type>String</item-type>
<label>Input</label>
<description>Current input of the speaker</description>
</channel-type>
<channel-type id="loopmode" advanced="true">
<item-type>String</item-type>
<label>Loop Mode</label>
<description>Loop mode of the speaker (ONE, ALL, NONE)</description>
<state>
<options>
<option value="NONE">No Repeat</option>
<option value="ONE">Repeat Track</option>
<option value="ALL">Repeat Playlist</option>
</options>
</state>
</channel-type>
<channel-type id="mute" advanced="true">
<item-type>Switch</item-type>
<label>Mute</label>
<description>Set or get the mute state of the speaker</description>
</channel-type>
<channel-type id="playstate" advanced="true">
<item-type>String</item-type>
<label>State</label>
<description>The State channel contains state of the Speaker, e.g. BUFFERING, PLAYING, STOPPED,...</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="shufflemode" advanced="true">
<item-type>Switch</item-type>
<label>Shuffle Mode</label>
<description>Toggle the shuffle mode of the speaker</description>
</channel-type>
<channel-type id="stop" advanced="true">
<item-type>Switch</item-type>
<label>Stop</label>
<description>Stop the current playback</description>
</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="volume">
<item-type>Dimmer</item-type>
<label>Volume</label>
<description>Set or get the master volume</description>
<category>SoundVolume</category>
</channel-type>
<channel-type id="volumecontrol" advanced="true">
<item-type>Switch</item-type>
<label>Volume Control</label>
<description>Flag if the volume control is enabled (might be disabled if speaker is not master of the zone)</description>
<category>SoundVolume</category>
<state readOnly="true"></state>
</channel-type>
<channel-type id="zoneid" advanced="true">
<item-type>String</item-type>
<label>Zone ID</label>
<description>Id of the Zone the speaker belongs to</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="zonemembers" advanced="true">
<item-type>String</item-type>
<label>Zone Members</label>
<description>Comma-separated list of zone members of this (lead) speaker</description>
</channel-type>
</thing:thing-descriptions>