added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
*
|
||||
*/
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user