added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.chromecast-${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-chromecast" description="Chromecast Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-transport-mdns</feature>
|
||||
<feature dependency="true">openhab.tp-jackson</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.chromecast/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 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.chromecast.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.audio.AudioFormat;
|
||||
import org.openhab.core.audio.AudioHTTPServer;
|
||||
import org.openhab.core.audio.AudioStream;
|
||||
import org.openhab.core.audio.FixedLengthAudioStream;
|
||||
import org.openhab.core.audio.URLAudioStream;
|
||||
import org.openhab.core.audio.UnsupportedAudioFormatException;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles the AudioSink portion of the Chromecast add-on.
|
||||
*
|
||||
* @author Jason Holmes - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastAudioSink {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastAudioSink.class);
|
||||
|
||||
private static final String MIME_TYPE_AUDIO_WAV = "audio/wav";
|
||||
private static final String MIME_TYPE_AUDIO_MPEG = "audio/mpeg";
|
||||
|
||||
private final ChromecastCommander commander;
|
||||
private final AudioHTTPServer audioHTTPServer;
|
||||
private final @Nullable String callbackUrl;
|
||||
|
||||
public ChromecastAudioSink(ChromecastCommander commander, AudioHTTPServer audioHTTPServer,
|
||||
@Nullable String callbackUrl) {
|
||||
this.commander = commander;
|
||||
this.audioHTTPServer = audioHTTPServer;
|
||||
this.callbackUrl = callbackUrl;
|
||||
}
|
||||
|
||||
public void process(@Nullable AudioStream audioStream) throws UnsupportedAudioFormatException {
|
||||
if (audioStream == null) {
|
||||
// in case the audioStream is null, this should be interpreted as a request to end any currently playing
|
||||
// stream.
|
||||
logger.trace("Stop currently playing stream.");
|
||||
commander.handleStop(OnOffType.ON);
|
||||
} else {
|
||||
final String url;
|
||||
if (audioStream instanceof URLAudioStream) {
|
||||
// it is an external URL, the speaker can access it itself and play it.
|
||||
URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
|
||||
url = urlAudioStream.getURL();
|
||||
} else {
|
||||
if (callbackUrl != null) {
|
||||
// we serve it on our own HTTP server
|
||||
String relativeUrl;
|
||||
if (audioStream instanceof FixedLengthAudioStream) {
|
||||
relativeUrl = audioHTTPServer.serve((FixedLengthAudioStream) audioStream, 10);
|
||||
} else {
|
||||
relativeUrl = audioHTTPServer.serve(audioStream);
|
||||
}
|
||||
url = callbackUrl + relativeUrl;
|
||||
} else {
|
||||
logger.warn("We do not have any callback url, so Chromecast cannot play the audio stream!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
commander.playMedia("Notification", url,
|
||||
AudioFormat.MP3.isCompatible(audioStream.getFormat()) ? MIME_TYPE_AUDIO_MPEG : MIME_TYPE_AUDIO_WAV);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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.chromecast.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link ChromecastBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Jason Holmes - Additional channels
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastBindingConstants {
|
||||
public static final String BINDING_ID = "chromecast";
|
||||
public static final String MEDIA_PLAYER = "CC1AD845";
|
||||
|
||||
public static final ThingTypeUID THING_TYPE_CHROMECAST = new ThingTypeUID(BINDING_ID, "chromecast");
|
||||
public static final ThingTypeUID THING_TYPE_AUDIO = new ThingTypeUID(BINDING_ID, "audio");
|
||||
public static final ThingTypeUID THING_TYPE_AUDIOGROUP = new ThingTypeUID(BINDING_ID, "audiogroup");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(
|
||||
Stream.of(THING_TYPE_AUDIO, THING_TYPE_AUDIOGROUP, THING_TYPE_CHROMECAST).collect(Collectors.toSet()));
|
||||
|
||||
// Config Parameters
|
||||
public static final String HOST = "ipAddress";
|
||||
public static final String PORT = "port";
|
||||
public static final String DEVICE_ID = "deviceId";
|
||||
|
||||
// Channel IDs
|
||||
public static final String CHANNEL_CONTROL = "control";
|
||||
public static final String CHANNEL_STOP = "stop";
|
||||
public static final String CHANNEL_VOLUME = "volume";
|
||||
public static final String CHANNEL_MUTE = "mute";
|
||||
public static final String CHANNEL_PLAY_URI = "playuri";
|
||||
|
||||
public static final String CHANNEL_APP_NAME = "appName";
|
||||
public static final String CHANNEL_APP_ID = "appId";
|
||||
public static final String CHANNEL_IDLING = "idling";
|
||||
public static final String CHANNEL_STATUS_TEXT = "statustext";
|
||||
|
||||
public static final String CHANNEL_CURRENT_TIME = "currentTime";
|
||||
public static final String CHANNEL_DURATION = "duration";
|
||||
|
||||
public static final String CHANNEL_METADATA_TYPE = "metadataType";
|
||||
|
||||
public static final String CHANNEL_ALBUM_ARTIST = "albumArtist";
|
||||
public static final String CHANNEL_ALBUM_NAME = "albumName";
|
||||
public static final String CHANNEL_ARTIST = "artist";
|
||||
public static final String CHANNEL_BROADCAST_DATE = "broadcastDate";
|
||||
public static final String CHANNEL_COMPOSER = "composer";
|
||||
public static final String CHANNEL_CREATION_DATE = "creationDate";
|
||||
public static final String CHANNEL_DISC_NUMBER = "discNumber";
|
||||
public static final String CHANNEL_EPISODE_NUMBER = "episodeNumber";
|
||||
public static final String CHANNEL_IMAGE = "image";
|
||||
public static final String CHANNEL_IMAGE_SRC = "imageSrc";
|
||||
public static final String CHANNEL_LOCATION_NAME = "locationName";
|
||||
public static final String CHANNEL_LOCATION = "location";
|
||||
public static final String CHANNEL_RELEASE_DATE = "releaseDate";
|
||||
public static final String CHANNEL_SEASON_NUMBER = "seasonNumber";
|
||||
public static final String CHANNEL_SERIES_TITLE = "seriesTitle";
|
||||
public static final String CHANNEL_STUDIO = "studio";
|
||||
public static final String CHANNEL_SUBTITLE = "subtitle";
|
||||
public static final String CHANNEL_TITLE = "title";
|
||||
public static final String CHANNEL_TRACK_NUMBER = "trackNumber";
|
||||
|
||||
/**
|
||||
* These are channels that map directly. Images and location are unique channels that
|
||||
* don't fit this description.
|
||||
*/
|
||||
public static final Set<String> METADATA_SIMPLE_CHANNELS = Collections
|
||||
.unmodifiableSet(Stream
|
||||
.of(CHANNEL_ALBUM_ARTIST, CHANNEL_ALBUM_NAME, CHANNEL_ARTIST, CHANNEL_BROADCAST_DATE,
|
||||
CHANNEL_COMPOSER, CHANNEL_CREATION_DATE, CHANNEL_DISC_NUMBER, CHANNEL_EPISODE_NUMBER,
|
||||
CHANNEL_LOCATION_NAME, CHANNEL_RELEASE_DATE, CHANNEL_SEASON_NUMBER, CHANNEL_SERIES_TITLE,
|
||||
CHANNEL_STUDIO, CHANNEL_SUBTITLE, CHANNEL_TITLE, CHANNEL_TRACK_NUMBER)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
// We don't key these metadata keys directly to a channel, they get linked together
|
||||
// into a Location channel.
|
||||
public static final String LOCATION_METADATA_LATITUDE = "locationLatitude";
|
||||
public static final String LOCATION_METADATA_LONGITUDE = "locationLongitude";
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* 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.chromecast.internal;
|
||||
|
||||
import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
|
||||
import static org.openhab.core.thing.ThingStatusDetail.COMMUNICATION_ERROR;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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.StringType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import su.litvak.chromecast.api.v2.Application;
|
||||
import su.litvak.chromecast.api.v2.ChromeCast;
|
||||
import su.litvak.chromecast.api.v2.MediaStatus;
|
||||
import su.litvak.chromecast.api.v2.Status;
|
||||
|
||||
/**
|
||||
* This sends the various commands to the Chromecast.
|
||||
*
|
||||
* @author Jason Holmes - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastCommander {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastCommander.class);
|
||||
|
||||
private final ChromeCast chromeCast;
|
||||
private final ChromecastScheduler scheduler;
|
||||
private final ChromecastStatusUpdater statusUpdater;
|
||||
|
||||
private static final int VOLUMESTEP = 10;
|
||||
|
||||
public ChromecastCommander(ChromeCast chromeCast, ChromecastScheduler scheduler,
|
||||
ChromecastStatusUpdater statusUpdater) {
|
||||
this.chromeCast = chromeCast;
|
||||
this.scheduler = scheduler;
|
||||
this.statusUpdater = statusUpdater;
|
||||
}
|
||||
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
scheduler.scheduleRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_CONTROL:
|
||||
handleControl(command);
|
||||
break;
|
||||
case CHANNEL_STOP:
|
||||
handleStop(command);
|
||||
break;
|
||||
case CHANNEL_VOLUME:
|
||||
handleVolume(command);
|
||||
break;
|
||||
case CHANNEL_MUTE:
|
||||
handleMute(command);
|
||||
break;
|
||||
case CHANNEL_PLAY_URI:
|
||||
handlePlayUri(command);
|
||||
break;
|
||||
default:
|
||||
logger.debug("Received command {} for unknown channel: {}", command, channelUID);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void handleRefresh() {
|
||||
if (!chromeCast.isConnected()) {
|
||||
scheduler.cancelRefresh();
|
||||
scheduler.scheduleConnect();
|
||||
return;
|
||||
}
|
||||
|
||||
Status status;
|
||||
try {
|
||||
status = chromeCast.getStatus();
|
||||
statusUpdater.processStatusUpdate(status);
|
||||
|
||||
if (status == null) {
|
||||
scheduler.cancelRefresh();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.debug("Failed to request status: {}", ex.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
|
||||
scheduler.cancelRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (status != null && status.getRunningApp() != null) {
|
||||
MediaStatus mediaStatus = chromeCast.getMediaStatus();
|
||||
statusUpdater.updateMediaStatus(mediaStatus);
|
||||
|
||||
if (mediaStatus != null && mediaStatus.playerState == MediaStatus.PlayerState.IDLE
|
||||
&& mediaStatus.idleReason != null
|
||||
&& mediaStatus.idleReason != MediaStatus.IdleReason.INTERRUPTED) {
|
||||
stopMediaPlayerApp();
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
logger.debug("Failed to request media status with a running app: {}", ex.getMessage());
|
||||
// We were just able to request status, so let's not put the device OFFLINE.
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePlayUri(Command command) {
|
||||
if (command instanceof StringType) {
|
||||
playMedia(null, command.toString(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleControl(final Command command) {
|
||||
try {
|
||||
if (command instanceof NextPreviousType) {
|
||||
// I can't find a way to control next/previous from the API. The Google app doesn't seem to
|
||||
// allow it either, so I suspect there isn't a way.
|
||||
logger.info("{} command not yet implemented", command);
|
||||
return;
|
||||
}
|
||||
|
||||
Application app = chromeCast.getRunningApp();
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
if (app == null) {
|
||||
logger.debug("{} command ignored because media player app is not running", command);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command instanceof PlayPauseType) {
|
||||
MediaStatus mediaStatus = chromeCast.getMediaStatus();
|
||||
logger.debug("mediaStatus {}", mediaStatus);
|
||||
if (mediaStatus == null || mediaStatus.playerState == MediaStatus.PlayerState.IDLE) {
|
||||
logger.debug("{} command ignored because media is not loaded", command);
|
||||
return;
|
||||
}
|
||||
|
||||
final PlayPauseType playPause = (PlayPauseType) command;
|
||||
if (playPause == PlayPauseType.PLAY) {
|
||||
chromeCast.play();
|
||||
} else if (playPause == PlayPauseType.PAUSE
|
||||
&& ((mediaStatus.supportedMediaCommands & 0x00000001) == 0x1)) {
|
||||
chromeCast.pause();
|
||||
} else {
|
||||
logger.info("{} command not supported by current media", command);
|
||||
}
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("{} command failed: {}", command, e.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void handleStop(final Command command) {
|
||||
if (command == OnOffType.ON) {
|
||||
try {
|
||||
chromeCast.stopApp();
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final IOException ex) {
|
||||
logger.debug("{} command failed: {}", command, ex.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void handleVolume(final Command command) {
|
||||
if (command instanceof PercentType) {
|
||||
setVolumeInternal((PercentType) command);
|
||||
} else if (command == IncreaseDecreaseType.INCREASE) {
|
||||
setVolumeInternal(new PercentType(
|
||||
Math.min(statusUpdater.getVolume().intValue() + VOLUMESTEP, PercentType.HUNDRED.intValue())));
|
||||
} else if (command == IncreaseDecreaseType.DECREASE) {
|
||||
setVolumeInternal(new PercentType(
|
||||
Math.max(statusUpdater.getVolume().intValue() - VOLUMESTEP, PercentType.ZERO.intValue())));
|
||||
}
|
||||
}
|
||||
|
||||
private void setVolumeInternal(PercentType volume) {
|
||||
try {
|
||||
chromeCast.setVolumeByIncrement(volume.floatValue() / 100);
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final IOException ex) {
|
||||
logger.debug("Set volume failed: {}", ex.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMute(final Command command) {
|
||||
if (command instanceof OnOffType) {
|
||||
final boolean mute = command == OnOffType.ON;
|
||||
try {
|
||||
chromeCast.setMuted(mute);
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final IOException ex) {
|
||||
logger.debug("Mute/unmute volume failed: {}", ex.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void playMedia(@Nullable String title, @Nullable String url, @Nullable String mimeType) {
|
||||
try {
|
||||
if (chromeCast.isAppAvailable(MEDIA_PLAYER)) {
|
||||
if (!chromeCast.isAppRunning(MEDIA_PLAYER)) {
|
||||
final Application app = chromeCast.launchApp(MEDIA_PLAYER);
|
||||
statusUpdater.setAppSessionId(app.sessionId);
|
||||
logger.debug("Application launched: {}", app);
|
||||
}
|
||||
if (url != null) {
|
||||
// If the current track is paused, launching a new request results in nothing happening, therefore
|
||||
// resume current track.
|
||||
MediaStatus ms = chromeCast.getMediaStatus();
|
||||
if (ms != null && MediaStatus.PlayerState.PAUSED == ms.playerState && url.equals(ms.media.url)) {
|
||||
logger.debug("Current stream paused, resuming");
|
||||
chromeCast.play();
|
||||
} else {
|
||||
chromeCast.load(title, null, url, mimeType);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.warn("Missing media player app - cannot process media.");
|
||||
}
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Failed playing media: {}", e.getMessage());
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void stopMediaPlayerApp() {
|
||||
try {
|
||||
Application app = chromeCast.getRunningApp();
|
||||
if (app.id.equals(MEDIA_PLAYER) && app.sessionId.equals(statusUpdater.getAppSessionId())) {
|
||||
chromeCast.stopApp();
|
||||
logger.debug("Media player app stopped");
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
logger.debug("Failed stopping media player app", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.chromecast.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import su.litvak.chromecast.api.v2.ChromeCastConnectionEvent;
|
||||
import su.litvak.chromecast.api.v2.ChromeCastConnectionEventListener;
|
||||
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent;
|
||||
import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEventListener;
|
||||
import su.litvak.chromecast.api.v2.MediaStatus;
|
||||
import su.litvak.chromecast.api.v2.Status;
|
||||
|
||||
/**
|
||||
* Responsible for listening to events from the Chromecast.
|
||||
*
|
||||
* @author Jason Holmes - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastEventReceiver implements ChromeCastSpontaneousEventListener, ChromeCastConnectionEventListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastEventReceiver.class);
|
||||
|
||||
private final ChromecastScheduler scheduler;
|
||||
private final ChromecastStatusUpdater statusUpdater;
|
||||
|
||||
public ChromecastEventReceiver(ChromecastScheduler scheduler, ChromecastStatusUpdater statusUpdater) {
|
||||
this.scheduler = scheduler;
|
||||
this.statusUpdater = statusUpdater;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connectionEventReceived(final @NonNullByDefault({}) ChromeCastConnectionEvent event) {
|
||||
if (event.isConnected()) {
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
scheduler.scheduleRefresh();
|
||||
} else {
|
||||
scheduler.cancelRefresh();
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE);
|
||||
// We might have just had a connection problem, let's try to reconnect.
|
||||
scheduler.scheduleConnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spontaneousEventReceived(final @NonNullByDefault({}) ChromeCastSpontaneousEvent event) {
|
||||
switch (event.getType()) {
|
||||
case CLOSE:
|
||||
statusUpdater.updateMediaStatus(null);
|
||||
break;
|
||||
case MEDIA_STATUS:
|
||||
statusUpdater.updateMediaStatus(event.getData(MediaStatus.class));
|
||||
break;
|
||||
case STATUS:
|
||||
statusUpdater.processStatusUpdate(event.getData(Status.class));
|
||||
break;
|
||||
case UNKNOWN:
|
||||
logger.debug("Received an 'UNKNOWN' event (class={})", event.getType().getDataClass());
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unhandled event type: {}", event.getType());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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.chromecast.internal;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Handles scheduling of connect and refresh events.
|
||||
*
|
||||
* @author Jason Holmes - Initial contribution
|
||||
* @author Wouter Born - Make sure only at most one refresh job is scheduled and running
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastScheduler {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastScheduler.class);
|
||||
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final long connectDelay;
|
||||
private final long refreshRate;
|
||||
private final Runnable connectRunnable;
|
||||
private final Runnable refreshRunnable;
|
||||
|
||||
private @Nullable ScheduledFuture<?> connectFuture;
|
||||
private @Nullable ScheduledFuture<?> refreshFuture;
|
||||
|
||||
public ChromecastScheduler(ScheduledExecutorService scheduler, long connectDelay, Runnable connectRunnable,
|
||||
long refreshRate, Runnable refreshRunnable) {
|
||||
this.scheduler = scheduler;
|
||||
this.connectDelay = connectDelay;
|
||||
this.connectRunnable = connectRunnable;
|
||||
this.refreshRate = refreshRate;
|
||||
this.refreshRunnable = refreshRunnable;
|
||||
}
|
||||
|
||||
public synchronized void destroy() {
|
||||
cancelConnect();
|
||||
cancelRefresh();
|
||||
}
|
||||
|
||||
public synchronized void scheduleConnect() {
|
||||
cancelConnect();
|
||||
logger.debug("Scheduling connection");
|
||||
connectFuture = scheduler.schedule(connectRunnable, connectDelay, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private synchronized void cancelConnect() {
|
||||
logger.debug("Canceling connection");
|
||||
ScheduledFuture<?> localConnectFuture = connectFuture;
|
||||
if (localConnectFuture != null) {
|
||||
localConnectFuture.cancel(true);
|
||||
connectFuture = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void scheduleRefresh() {
|
||||
cancelRefresh();
|
||||
logger.debug("Scheduling refresh in {} seconds", refreshRate);
|
||||
// With an initial delay of 1 second the refresh job can be restarted when several channels are refreshed at
|
||||
// once e.g. due to channel linking
|
||||
refreshFuture = scheduler.scheduleWithFixedDelay(refreshRunnable, 1, refreshRate, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public synchronized void cancelRefresh() {
|
||||
logger.debug("Canceling refresh");
|
||||
ScheduledFuture<?> localRefreshFuture = refreshFuture;
|
||||
if (localRefreshFuture != null) {
|
||||
localRefreshFuture.cancel(true);
|
||||
refreshFuture = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/**
|
||||
* 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.chromecast.internal;
|
||||
|
||||
import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
|
||||
import static su.litvak.chromecast.api.v2.MediaStatus.PlayerState.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.chromecast.internal.handler.ChromecastHandler;
|
||||
import org.openhab.binding.chromecast.internal.utils.ByteArrayFileCache;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
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.PointType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.RawType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import su.litvak.chromecast.api.v2.Application;
|
||||
import su.litvak.chromecast.api.v2.Media;
|
||||
import su.litvak.chromecast.api.v2.MediaStatus;
|
||||
import su.litvak.chromecast.api.v2.Status;
|
||||
import su.litvak.chromecast.api.v2.Volume;
|
||||
|
||||
/**
|
||||
* Responsible for updating the Thing status based on messages received from a ChromeCast. This doesn't query anything -
|
||||
* it just parses the messages and updates the Thing. Message handling/scheduling/receiving is done elsewhere.
|
||||
* <p>
|
||||
* This also maintains state of both volume and the appSessionId (only if we started playing media).
|
||||
*
|
||||
* @author Jason Holmes - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastStatusUpdater {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastStatusUpdater.class);
|
||||
|
||||
private final Thing thing;
|
||||
private final ChromecastHandler callback;
|
||||
private static final ByteArrayFileCache IMAGE_CACHE = new ByteArrayFileCache("org.openhab.binding.chromecast");
|
||||
|
||||
private @Nullable String appSessionId;
|
||||
private PercentType volume = PercentType.ZERO;
|
||||
|
||||
public ChromecastStatusUpdater(Thing thing, ChromecastHandler callback) {
|
||||
this.thing = thing;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public PercentType getVolume() {
|
||||
return volume;
|
||||
}
|
||||
|
||||
public @Nullable String getAppSessionId() {
|
||||
return appSessionId;
|
||||
}
|
||||
|
||||
public void setAppSessionId(String appSessionId) {
|
||||
this.appSessionId = appSessionId;
|
||||
}
|
||||
|
||||
public void processStatusUpdate(final @Nullable Status status) {
|
||||
if (status == null) {
|
||||
updateStatus(ThingStatus.OFFLINE);
|
||||
updateAppStatus(null);
|
||||
updateVolumeStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (status.applications == null) {
|
||||
this.appSessionId = null;
|
||||
}
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
updateAppStatus(status.getRunningApp());
|
||||
updateVolumeStatus(status.volume);
|
||||
}
|
||||
|
||||
public void updateAppStatus(final @Nullable Application application) {
|
||||
State name = UnDefType.UNDEF;
|
||||
State id = UnDefType.UNDEF;
|
||||
State statusText = UnDefType.UNDEF;
|
||||
OnOffType idling = OnOffType.ON;
|
||||
|
||||
if (application != null) {
|
||||
name = new StringType(application.name);
|
||||
id = new StringType(application.id);
|
||||
statusText = new StringType(application.statusText);
|
||||
idling = application.isIdleScreen ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
callback.updateState(CHANNEL_APP_NAME, name);
|
||||
callback.updateState(CHANNEL_APP_ID, id);
|
||||
callback.updateState(CHANNEL_STATUS_TEXT, statusText);
|
||||
callback.updateState(CHANNEL_IDLING, idling);
|
||||
}
|
||||
|
||||
public void updateVolumeStatus(final @Nullable Volume volume) {
|
||||
if (volume == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
PercentType value = new PercentType((int) (volume.level * 100));
|
||||
this.volume = value;
|
||||
|
||||
callback.updateState(CHANNEL_VOLUME, value);
|
||||
callback.updateState(CHANNEL_MUTE, volume.muted ? OnOffType.ON : OnOffType.OFF);
|
||||
}
|
||||
|
||||
public void updateMediaStatus(final @Nullable MediaStatus mediaStatus) {
|
||||
logger.debug("MEDIA_STATUS {}", mediaStatus);
|
||||
|
||||
// In-between songs? It's thinking? It's not doing anything
|
||||
if (mediaStatus == null) {
|
||||
callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
|
||||
callback.updateState(CHANNEL_STOP, OnOffType.ON);
|
||||
callback.updateState(CHANNEL_CURRENT_TIME, UnDefType.UNDEF);
|
||||
updateMediaInfoStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (mediaStatus.playerState) {
|
||||
case IDLE:
|
||||
break;
|
||||
case PAUSED:
|
||||
callback.updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE);
|
||||
callback.updateState(CHANNEL_STOP, OnOffType.OFF);
|
||||
break;
|
||||
case BUFFERING:
|
||||
case LOADING:
|
||||
case PLAYING:
|
||||
callback.updateState(CHANNEL_CONTROL, PlayPauseType.PLAY);
|
||||
callback.updateState(CHANNEL_STOP, OnOffType.OFF);
|
||||
break;
|
||||
default:
|
||||
logger.debug("Unknown media status: {}", mediaStatus.playerState);
|
||||
break;
|
||||
}
|
||||
|
||||
callback.updateState(CHANNEL_CURRENT_TIME, new QuantityType<>(mediaStatus.currentTime, SmartHomeUnits.SECOND));
|
||||
|
||||
// If we're playing, paused or buffering but don't have any MEDIA information don't null everything out.
|
||||
Media media = mediaStatus.media;
|
||||
if (media == null && (mediaStatus.playerState == PLAYING || mediaStatus.playerState == PAUSED
|
||||
|| mediaStatus.playerState == BUFFERING)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateMediaInfoStatus(media);
|
||||
}
|
||||
|
||||
private void updateMediaInfoStatus(final @Nullable Media media) {
|
||||
State duration = UnDefType.UNDEF;
|
||||
String metadataType = Media.MetadataType.GENERIC.name();
|
||||
if (media != null) {
|
||||
metadataType = media.getMetadataType().name();
|
||||
|
||||
// duration can be null when a new song is about to play.
|
||||
if (media.duration != null) {
|
||||
duration = new QuantityType<>(media.duration, SmartHomeUnits.SECOND);
|
||||
}
|
||||
}
|
||||
|
||||
callback.updateState(CHANNEL_DURATION, duration);
|
||||
callback.updateState(CHANNEL_METADATA_TYPE, new StringType(metadataType));
|
||||
|
||||
updateMetadataStatus(media == null || media.metadata == null ? Collections.emptyMap() : media.metadata);
|
||||
}
|
||||
|
||||
private void updateMetadataStatus(Map<String, Object> metadata) {
|
||||
updateLocation(metadata);
|
||||
updateImage(metadata);
|
||||
|
||||
thing.getChannels().stream() //
|
||||
.map(channel -> channel.getUID())
|
||||
.filter(channelUID -> METADATA_SIMPLE_CHANNELS.contains(channelUID.getId()))
|
||||
.forEach(channelUID -> updateChannel(channelUID, metadata));
|
||||
}
|
||||
|
||||
/** Lat/lon are combined into 1 channel so we have to handle them as a special case. */
|
||||
private void updateLocation(Map<String, Object> metadata) {
|
||||
if (!callback.isLinked(CHANNEL_LOCATION)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Double lat = (Double) metadata.get(LOCATION_METADATA_LATITUDE);
|
||||
Double lon = (Double) metadata.get(LOCATION_METADATA_LONGITUDE);
|
||||
if (lat == null || lon == null) {
|
||||
callback.updateState(CHANNEL_LOCATION, UnDefType.UNDEF);
|
||||
} else {
|
||||
PointType pointType = new PointType(new DecimalType(lat), new DecimalType(lon));
|
||||
callback.updateState(CHANNEL_LOCATION, pointType);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateImage(Map<String, Object> metadata) {
|
||||
if (!(callback.isLinked(CHANNEL_IMAGE) || (callback.isLinked(CHANNEL_IMAGE_SRC)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Channel name and metadata key don't match.
|
||||
Object imagesValue = metadata.get("images");
|
||||
if (imagesValue == null) {
|
||||
callback.updateState(CHANNEL_IMAGE_SRC, UnDefType.UNDEF);
|
||||
return;
|
||||
}
|
||||
|
||||
String imageSrc = null;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, String>> strings = (List<Map<String, String>>) imagesValue;
|
||||
for (Map<String, String> stringMap : strings) {
|
||||
String url = stringMap.get("url");
|
||||
if (url != null) {
|
||||
imageSrc = url;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (callback.isLinked(CHANNEL_IMAGE_SRC)) {
|
||||
callback.updateState(CHANNEL_IMAGE_SRC, imageSrc == null ? UnDefType.UNDEF : new StringType(imageSrc));
|
||||
}
|
||||
|
||||
if (callback.isLinked(CHANNEL_IMAGE)) {
|
||||
State image = imageSrc == null ? UnDefType.UNDEF : downloadImageFromCache(imageSrc);
|
||||
callback.updateState(CHANNEL_IMAGE, image == null ? UnDefType.UNDEF : image);
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable RawType downloadImage(String url) {
|
||||
logger.debug("Trying to download the content of URL '{}'", url);
|
||||
RawType downloadedImage = HttpUtil.downloadImage(url);
|
||||
if (downloadedImage == null) {
|
||||
logger.debug("Failed to download the content of URL '{}'", url);
|
||||
}
|
||||
return downloadedImage;
|
||||
}
|
||||
|
||||
private @Nullable RawType downloadImageFromCache(String url) {
|
||||
if (IMAGE_CACHE.containsKey(url)) {
|
||||
try {
|
||||
byte[] bytes = IMAGE_CACHE.get(url);
|
||||
String contentType = HttpUtil.guessContentTypeFromData(bytes);
|
||||
return new RawType(bytes,
|
||||
contentType == null || contentType.isEmpty() ? RawType.DEFAULT_MIME_TYPE : contentType);
|
||||
} catch (IOException e) {
|
||||
logger.trace("Failed to download the content of URL '{}'", url, e);
|
||||
}
|
||||
} else {
|
||||
RawType image = downloadImage(url);
|
||||
if (image != null) {
|
||||
IMAGE_CACHE.put(url, image.getBytes());
|
||||
return image;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateChannel(ChannelUID channelUID, Map<String, Object> metadata) {
|
||||
if (!callback.isLinked(channelUID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object value = getValue(channelUID.getId(), metadata);
|
||||
State state;
|
||||
|
||||
if (value == null) {
|
||||
state = UnDefType.UNDEF;
|
||||
} else if (value instanceof Double) {
|
||||
state = new DecimalType((Double) value);
|
||||
} else if (value instanceof Integer) {
|
||||
state = new DecimalType(((Integer) value).longValue());
|
||||
} else if (value instanceof String) {
|
||||
state = new StringType(value.toString());
|
||||
} else if (value instanceof ZonedDateTime) {
|
||||
state = new DateTimeType((ZonedDateTime) value);
|
||||
} else {
|
||||
state = UnDefType.UNDEF;
|
||||
logger.warn("Update channel {}: Unsupported value type {}", channelUID, value.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
callback.updateState(channelUID, state);
|
||||
}
|
||||
|
||||
private @Nullable Object getValue(String channelId, @Nullable Map<String, Object> metadata) {
|
||||
if (metadata == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (CHANNEL_BROADCAST_DATE.equals(channelId) || CHANNEL_RELEASE_DATE.equals(channelId)
|
||||
|| CHANNEL_CREATION_DATE.equals(channelId)) {
|
||||
String dateString = (String) metadata.get(channelId);
|
||||
return (dateString == null) ? null
|
||||
: ZonedDateTime.ofInstant(Instant.parse(dateString), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
return metadata.get(channelId);
|
||||
}
|
||||
|
||||
public void updateStatus(ThingStatus status) {
|
||||
updateStatus(status, ThingStatusDetail.NONE, null);
|
||||
}
|
||||
|
||||
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
|
||||
callback.updateStatus(status, statusDetail, description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.chromecast.internal.config;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Thing configuration from openHAB.
|
||||
*
|
||||
* @author Christoph Weitkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastConfig {
|
||||
public @Nullable String ipAddress = null;
|
||||
public int port = 8009;
|
||||
public long refreshRate = 10;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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.chromecast.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.jmdns.ServiceInfo;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link ChromecastDiscoveryParticipant} is responsible for discovering Chromecast devices through UPnP.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
* @author Daniel Walters - Change discovery protocol to mDNS
|
||||
*/
|
||||
@Component(immediate = true)
|
||||
@NonNullByDefault
|
||||
public class ChromecastDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastDiscoveryParticipant.class);
|
||||
|
||||
private static final String PROPERTY_MODEL = "md";
|
||||
private static final String PROPERTY_FRIENDLY_NAME = "fn";
|
||||
private static final String PROPERTY_DEVICE_ID = "id";
|
||||
private static final String SERVICE_TYPE = "_googlecast._tcp.local.";
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceType() {
|
||||
return SERVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
|
||||
final ThingUID uid = getThingUID(service);
|
||||
if (uid == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, Object> properties = new HashMap<>(5);
|
||||
String host = service.getHostAddresses()[0];
|
||||
properties.put(HOST, host);
|
||||
int port = service.getPort();
|
||||
properties.put(PORT, port);
|
||||
logger.debug("Chromecast Found: {} {}", host, port);
|
||||
String id = service.getPropertyString(PROPERTY_DEVICE_ID);
|
||||
properties.put(DEVICE_ID, id);
|
||||
String friendlyName = service.getPropertyString(PROPERTY_FRIENDLY_NAME); // friendly name;
|
||||
|
||||
final DiscoveryResult result = DiscoveryResultBuilder.create(uid).withThingType(getThingType(service))
|
||||
.withProperties(properties).withRepresentationProperty(DEVICE_ID).withLabel(friendlyName).build();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private @Nullable ThingTypeUID getThingType(final ServiceInfo service) {
|
||||
String model = service.getPropertyString(PROPERTY_MODEL); // model
|
||||
logger.debug("Chromecast Type: {}", model);
|
||||
if (model == null) {
|
||||
return null;
|
||||
}
|
||||
if (model.equals("Chromecast Audio")) {
|
||||
return THING_TYPE_AUDIO;
|
||||
} else if (model.equals("Google Cast Group")) {
|
||||
return THING_TYPE_AUDIOGROUP;
|
||||
} else {
|
||||
return THING_TYPE_CHROMECAST;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingUID getThingUID(ServiceInfo service) {
|
||||
ThingTypeUID thingTypeUID = getThingType(service);
|
||||
if (thingTypeUID != null) {
|
||||
String id = service.getPropertyString(PROPERTY_DEVICE_ID); // device id
|
||||
return new ThingUID(thingTypeUID, id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* 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.chromecast.internal.factory;
|
||||
|
||||
import static org.openhab.binding.chromecast.internal.ChromecastBindingConstants.SUPPORTED_THING_TYPES_UIDS;
|
||||
|
||||
import java.util.Dictionary;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.chromecast.internal.handler.ChromecastHandler;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* The {@link ChromecastHandlerFactory} is responsible for creating things and thing handlers.
|
||||
*
|
||||
* @author Kai Kreuzer - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.chromecast")
|
||||
@NonNullByDefault
|
||||
public class ChromecastHandlerFactory extends BaseThingHandlerFactory {
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastHandlerFactory.class);
|
||||
|
||||
private final Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();
|
||||
private final AudioHTTPServer audioHTTPServer;
|
||||
private final NetworkAddressService networkAddressService;
|
||||
|
||||
/** url (scheme+server+port) to use for playing notification sounds. */
|
||||
private @Nullable String callbackUrl;
|
||||
|
||||
@Activate
|
||||
public ChromecastHandlerFactory(final @Reference AudioHTTPServer audioHTTPServer,
|
||||
final @Reference NetworkAddressService networkAddressService) {
|
||||
logger.debug("Creating new instance of ChromecastHandlerFactory");
|
||||
this.audioHTTPServer = audioHTTPServer;
|
||||
this.networkAddressService = networkAddressService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(ComponentContext componentContext) {
|
||||
super.activate(componentContext);
|
||||
Dictionary<String, Object> properties = componentContext.getProperties();
|
||||
callbackUrl = (String) properties.get("callbackUrl");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ChromecastHandler handler = new ChromecastHandler(thing, audioHTTPServer, createCallbackUrl());
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ServiceRegistration<AudioSink> reg = (ServiceRegistration<AudioSink>) bundleContext
|
||||
.registerService(AudioSink.class.getName(), handler, null);
|
||||
audioSinkRegistrations.put(thing.getUID().toString(), reg);
|
||||
|
||||
return handler;
|
||||
}
|
||||
|
||||
private @Nullable String createCallbackUrl() {
|
||||
if (callbackUrl != null) {
|
||||
return callbackUrl;
|
||||
} else {
|
||||
final 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;
|
||||
}
|
||||
|
||||
return "http://" + ipAddress + ":" + port;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHandler(Thing thing) {
|
||||
super.unregisterHandler(thing);
|
||||
ServiceRegistration<AudioSink> reg = audioSinkRegistrations.get(thing.getUID().toString());
|
||||
reg.unregister();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 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.chromecast.internal.handler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.chromecast.internal.ChromecastAudioSink;
|
||||
import org.openhab.binding.chromecast.internal.ChromecastCommander;
|
||||
import org.openhab.binding.chromecast.internal.ChromecastEventReceiver;
|
||||
import org.openhab.binding.chromecast.internal.ChromecastScheduler;
|
||||
import org.openhab.binding.chromecast.internal.ChromecastStatusUpdater;
|
||||
import org.openhab.binding.chromecast.internal.config.ChromecastConfig;
|
||||
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.UnsupportedAudioFormatException;
|
||||
import org.openhab.core.audio.UnsupportedAudioStreamException;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import su.litvak.chromecast.api.v2.ChromeCast;
|
||||
|
||||
/**
|
||||
* The {@link ChromecastHandler} is responsible for handling commands, which are sent to one of the channels. It
|
||||
* furthermore implements {@link AudioSink} support.
|
||||
*
|
||||
* @author Markus Rathgeb, Kai Kreuzer - Initial contribution
|
||||
* @author Daniel Walters - Online status fix, handle playuri channel and refactor play media code
|
||||
* @author Jason Holmes - Media Status. Refactor the monolith into separate classes.
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ChromecastHandler extends BaseThingHandler implements AudioSink {
|
||||
|
||||
private static final Set<AudioFormat> SUPPORTED_FORMATS = Collections
|
||||
.unmodifiableSet(Stream.of(AudioFormat.MP3, AudioFormat.WAV).collect(Collectors.toSet()));
|
||||
private static final Set<Class<? extends AudioStream>> SUPPORTED_STREAMS = Collections.singleton(AudioStream.class);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ChromecastHandler.class);
|
||||
private final AudioHTTPServer audioHTTPServer;
|
||||
private final @Nullable String callbackUrl;
|
||||
|
||||
/**
|
||||
* The actual implementation. A new one is created each time #initialize is called.
|
||||
*/
|
||||
private @Nullable Coordinator coordinator;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param thing the thing the coordinator should be created for
|
||||
* @param audioHTTPServer server for hosting audio streams
|
||||
* @param callbackUrl url to be used to tell the Chromecast which host to call for audio urls
|
||||
*/
|
||||
public ChromecastHandler(final Thing thing, AudioHTTPServer audioHTTPServer, @Nullable String callbackUrl) {
|
||||
super(thing);
|
||||
this.audioHTTPServer = audioHTTPServer;
|
||||
this.callbackUrl = callbackUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
ChromecastConfig config = getConfigAs(ChromecastConfig.class);
|
||||
|
||||
final String ipAddress = config.ipAddress;
|
||||
if (ipAddress == null || ipAddress.isEmpty()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
|
||||
"Cannot connect to Chromecast. IP address is not valid or missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null && (!localCoordinator.chromeCast.getAddress().equals(ipAddress)
|
||||
|| (localCoordinator.chromeCast.getPort() != config.port))) {
|
||||
localCoordinator.destroy();
|
||||
localCoordinator = coordinator = null;
|
||||
}
|
||||
|
||||
if (localCoordinator == null) {
|
||||
ChromeCast chromecast = new ChromeCast(ipAddress, config.port);
|
||||
localCoordinator = new Coordinator(this, thing, chromecast, config.refreshRate, audioHTTPServer,
|
||||
callbackUrl);
|
||||
localCoordinator.initialize();
|
||||
coordinator = localCoordinator;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null) {
|
||||
localCoordinator.destroy();
|
||||
coordinator = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final ChannelUID channelUID, final Command command) {
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null) {
|
||||
localCoordinator.commander.handleCommand(channelUID, command);
|
||||
} else {
|
||||
logger.debug("Cannot handle command. No coordinator has been initialized");
|
||||
}
|
||||
}
|
||||
|
||||
@Override // Just exposing this for ChromecastStatusUpdater.
|
||||
public void updateState(String channelId, State state) {
|
||||
super.updateState(channelId, state);
|
||||
}
|
||||
|
||||
@Override // Just exposing this for ChromecastStatusUpdater.
|
||||
public void updateState(ChannelUID channelUID, State state) {
|
||||
super.updateState(channelUID, state);
|
||||
}
|
||||
|
||||
@Override // Just exposing this for ChromecastStatusUpdater.
|
||||
public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
|
||||
super.updateStatus(status, statusDetail, description);
|
||||
}
|
||||
|
||||
@Override // Just exposing this for ChromecastStatusUpdater.
|
||||
public boolean isLinked(String channelId) {
|
||||
return super.isLinked(channelId);
|
||||
}
|
||||
|
||||
@Override // Just exposing this for ChromecastStatusUpdater.
|
||||
public boolean isLinked(ChannelUID channelUID) {
|
||||
return super.isLinked(channelUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return thing.getUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getLabel(@Nullable Locale locale) {
|
||||
return thing.getLabel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<AudioFormat> getSupportedFormats() {
|
||||
return SUPPORTED_FORMATS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<? extends AudioStream>> getSupportedStreams() {
|
||||
return SUPPORTED_STREAMS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void process(@Nullable AudioStream audioStream)
|
||||
throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null) {
|
||||
localCoordinator.audioSink.process(audioStream);
|
||||
} else {
|
||||
logger.debug("Cannot process audioStream. No coordinator has been initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PercentType getVolume() throws IOException {
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null) {
|
||||
return localCoordinator.statusUpdater.getVolume();
|
||||
} else {
|
||||
throw new IOException("Cannot get volume. No coordinator has been initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(PercentType percentType) throws IOException {
|
||||
Coordinator localCoordinator = coordinator;
|
||||
if (localCoordinator != null) {
|
||||
localCoordinator.commander.handleVolume(percentType);
|
||||
} else {
|
||||
throw new IOException("Cannot set volume. No coordinator has been initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
private static class Coordinator {
|
||||
private final Logger logger = LoggerFactory.getLogger(Coordinator.class);
|
||||
|
||||
private static final long CONNECT_DELAY = 10;
|
||||
|
||||
private final ChromeCast chromeCast;
|
||||
private final ChromecastAudioSink audioSink;
|
||||
private final ChromecastCommander commander;
|
||||
private final ChromecastEventReceiver eventReceiver;
|
||||
private final ChromecastStatusUpdater statusUpdater;
|
||||
private final ChromecastScheduler scheduler;
|
||||
|
||||
private Coordinator(ChromecastHandler handler, Thing thing, ChromeCast chromeCast, long refreshRate,
|
||||
AudioHTTPServer audioHttpServer, @Nullable String callbackURL) {
|
||||
this.chromeCast = chromeCast;
|
||||
|
||||
this.scheduler = new ChromecastScheduler(handler.scheduler, CONNECT_DELAY, this::connect, refreshRate,
|
||||
this::refresh);
|
||||
this.statusUpdater = new ChromecastStatusUpdater(thing, handler);
|
||||
|
||||
this.commander = new ChromecastCommander(chromeCast, scheduler, statusUpdater);
|
||||
this.eventReceiver = new ChromecastEventReceiver(scheduler, statusUpdater);
|
||||
this.audioSink = new ChromecastAudioSink(commander, audioHttpServer, callbackURL);
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
chromeCast.registerListener(eventReceiver);
|
||||
chromeCast.registerConnectionListener(eventReceiver);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
void destroy() {
|
||||
chromeCast.unregisterConnectionListener(eventReceiver);
|
||||
chromeCast.unregisterListener(eventReceiver);
|
||||
|
||||
try {
|
||||
scheduler.destroy();
|
||||
chromeCast.disconnect();
|
||||
} catch (final IOException ex) {
|
||||
logger.debug("Disconnect failed: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void connect() {
|
||||
try {
|
||||
chromeCast.connect();
|
||||
statusUpdater.updateMediaStatus(null);
|
||||
statusUpdater.updateStatus(ThingStatus.ONLINE);
|
||||
} catch (final Exception e) {
|
||||
statusUpdater.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
e.getMessage());
|
||||
scheduler.scheduleConnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
commander.handleRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* 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.chromecast.internal.utils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.core.ConfigConstants;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This is a simple file based cache implementation.
|
||||
*
|
||||
* @author Christoph Weitkamp - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ByteArrayFileCache {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ByteArrayFileCache.class);
|
||||
|
||||
private static final String MD5_ALGORITHM = "MD5";
|
||||
|
||||
static final String CACHE_FOLDER_NAME = "cache";
|
||||
private static final char EXTENSION_SEPARATOR = '.';
|
||||
private static final char UNIX_SEPARATOR = '/';
|
||||
private static final char WINDOWS_SEPARATOR = '\\';
|
||||
|
||||
private final File cacheFolder;
|
||||
|
||||
static final long ONE_DAY_IN_MILLIS = TimeUnit.DAYS.toMillis(1);
|
||||
private int expiry = 0;
|
||||
|
||||
private static final Map<String, File> FILES_IN_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
|
||||
* <code>$userdata/cache/$servicePID</code>.
|
||||
*
|
||||
* @param servicePID PID of the service
|
||||
*/
|
||||
public ByteArrayFileCache(String servicePID) {
|
||||
// TODO track and limit folder size
|
||||
// TODO support user specific folder
|
||||
cacheFolder = new File(new File(ConfigConstants.getUserDataFolder(), CACHE_FOLDER_NAME), servicePID);
|
||||
if (!cacheFolder.exists()) {
|
||||
logger.debug("Creating cache folder '{}'", cacheFolder.getAbsolutePath());
|
||||
cacheFolder.mkdirs();
|
||||
}
|
||||
logger.debug("Using cache folder '{}'", cacheFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ByteArrayFileCache} instance for a service. Creates a <code>cache</code> folder under
|
||||
* <code>$userdata/cache/$servicePID/</code>.
|
||||
*
|
||||
* @param servicePID PID of the service
|
||||
* @param int the days for how long the files stay in the cache valid. Must be positive. 0 to
|
||||
* disables this functionality.
|
||||
*/
|
||||
public ByteArrayFileCache(String servicePID, int expiry) {
|
||||
this(servicePID);
|
||||
if (expiry < 0) {
|
||||
throw new IllegalArgumentException("Cache expiration time must be greater than or equal to 0");
|
||||
}
|
||||
this.expiry = expiry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the cache. If the cache previously contained a file for the key, the old file is replaced by the
|
||||
* new content.
|
||||
*
|
||||
* @param key the key with which the file is to be associated
|
||||
* @param content the content for the file to be associated with the specified key
|
||||
*/
|
||||
public void put(String key, byte[] content) {
|
||||
writeFile(getUniqueFile(key), content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the cache.
|
||||
*
|
||||
* @param key the key with which the file is to be associated
|
||||
* @param content the content for the file to be associated with the specified key
|
||||
*/
|
||||
public void putIfAbsent(String key, byte[] content) {
|
||||
File fileInCache = getUniqueFile(key);
|
||||
if (fileInCache.exists()) {
|
||||
logger.debug("File '{}' present in cache", fileInCache.getName());
|
||||
// update time of last use
|
||||
fileInCache.setLastModified(System.currentTimeMillis());
|
||||
} else {
|
||||
writeFile(fileInCache, content);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a file to the cache and returns the content of the file.
|
||||
*
|
||||
* @param key the key with which the file is to be associated
|
||||
* @param content the content for the file to be associated with the specified key
|
||||
* @return the content of the file associated with the given key
|
||||
*/
|
||||
public byte[] putIfAbsentAndGet(String key, byte[] content) {
|
||||
putIfAbsent(key, content);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given content to the given {@link File}.
|
||||
*
|
||||
* @param fileInCache the {@link File}
|
||||
* @param content the content to be written
|
||||
*/
|
||||
private void writeFile(File fileInCache, byte[] content) {
|
||||
logger.debug("Caching file '{}'", fileInCache.getName());
|
||||
try {
|
||||
Files.write(fileInCache.toPath(), content);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not write file '{}' to cache", fileInCache.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the key is present in the cache.
|
||||
*
|
||||
* @param key the key whose presence in the cache is to be tested
|
||||
* @return true if the cache contains a file for the specified key
|
||||
*/
|
||||
public boolean containsKey(String key) {
|
||||
return getUniqueFile(key).exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file associated with the given key from the cache.
|
||||
*
|
||||
* @param key the key whose associated file is to be removed
|
||||
*/
|
||||
public void remove(String key) {
|
||||
deleteFile(getUniqueFile(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given {@link File}.
|
||||
*
|
||||
* @param fileInCache the {@link File}
|
||||
*/
|
||||
private void deleteFile(File fileInCache) {
|
||||
if (fileInCache.exists()) {
|
||||
logger.debug("Deleting file '{}' from cache", fileInCache.getName());
|
||||
fileInCache.delete();
|
||||
} else {
|
||||
logger.debug("File '{}' not found in cache", fileInCache.getName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all files from the cache.
|
||||
*/
|
||||
public void clear() {
|
||||
File[] filesInCache = cacheFolder.listFiles();
|
||||
if (filesInCache != null && filesInCache.length > 0) {
|
||||
logger.debug("Deleting all files from cache");
|
||||
Arrays.stream(filesInCache).forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes expired files from the cache.
|
||||
*/
|
||||
public void clearExpired() {
|
||||
// exit if expiry is set to 0 (disabled)
|
||||
if (expiry <= 0) {
|
||||
return;
|
||||
}
|
||||
File[] filesInCache = cacheFolder.listFiles();
|
||||
if (filesInCache != null && filesInCache.length > 0) {
|
||||
logger.debug("Deleting expired files from cache");
|
||||
Arrays.stream(filesInCache).filter(file -> isExpired(file)).forEach(File::delete);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given {@link File} is expired.
|
||||
*
|
||||
* @param fileInCache the {@link File}
|
||||
* @return <code>true</code> if the file is expired, <code>false</code> otherwise
|
||||
*/
|
||||
private boolean isExpired(File fileInCache) {
|
||||
// exit if expiry is set to 0 (disabled)
|
||||
if (expiry <= 0) {
|
||||
return false;
|
||||
}
|
||||
return expiry * ONE_DAY_IN_MILLIS < System.currentTimeMillis() - fileInCache.lastModified();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the file associated with the given key, if it is present.
|
||||
*
|
||||
* @param key the key whose associated file is to be returned
|
||||
* @return the content of the file associated with the given key
|
||||
* @throws FileNotFoundException if the given file could not be found in cache
|
||||
* @throws IOException if an I/O error occurs reading the given file
|
||||
*/
|
||||
public byte[] get(String key) throws FileNotFoundException, IOException {
|
||||
return readFile(getUniqueFile(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the content from the given {@link File}, if it is present.
|
||||
*
|
||||
* @param fileInCache the {@link File}
|
||||
* @return the content of the file
|
||||
* @throws FileNotFoundException if the given file could not be found in cache
|
||||
* @throws IOException if an I/O error occurs reading the given file
|
||||
*/
|
||||
private byte[] readFile(File fileInCache) throws FileNotFoundException, IOException {
|
||||
if (fileInCache.exists()) {
|
||||
logger.debug("Reading file '{}' from cache", fileInCache.getName());
|
||||
// update time of last use
|
||||
fileInCache.setLastModified(System.currentTimeMillis());
|
||||
try {
|
||||
return Files.readAllBytes(fileInCache.toPath());
|
||||
} catch (IOException e) {
|
||||
logger.warn("Could not read file '{}' from cache", fileInCache.getName(), e);
|
||||
throw new IOException(String.format("Could not read file '%s' from cache", fileInCache.getName()));
|
||||
}
|
||||
} else {
|
||||
logger.debug("File '{}' not found in cache", fileInCache.getName());
|
||||
throw new FileNotFoundException(String.format("File '%s' not found in cache", fileInCache.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique {@link File} from the key with which the file is to be associated.
|
||||
*
|
||||
* @param key the key with which the file is to be associated
|
||||
* @return unique file for the file associated with the given key
|
||||
*/
|
||||
File getUniqueFile(String key) {
|
||||
String uniqueFileName = getUniqueFileName(key);
|
||||
if (FILES_IN_CACHE.containsKey(uniqueFileName)) {
|
||||
return FILES_IN_CACHE.get(uniqueFileName);
|
||||
} else {
|
||||
String fileExtension = getFileExtension(key);
|
||||
File fileInCache = new File(cacheFolder,
|
||||
uniqueFileName + (fileExtension == null ? "" : EXTENSION_SEPARATOR + fileExtension));
|
||||
FILES_IN_CACHE.put(uniqueFileName, fileInCache);
|
||||
return fileInCache;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension of a file name.
|
||||
*
|
||||
* @param fileName the file name to retrieve the extension of
|
||||
* @return the extension of the file or null if none exists
|
||||
*/
|
||||
@Nullable
|
||||
String getFileExtension(String fileName) {
|
||||
int extensionPos = fileName.lastIndexOf(EXTENSION_SEPARATOR);
|
||||
int lastSeparatorPos = Math.max(fileName.lastIndexOf(UNIX_SEPARATOR), fileName.lastIndexOf(WINDOWS_SEPARATOR));
|
||||
return lastSeparatorPos > extensionPos ? null : fileName.substring(extensionPos + 1).replaceFirst("\\?.*$", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a unique file name from the key with which the file is to be associated.
|
||||
*
|
||||
* @param key the key with which the file is to be associated
|
||||
* @return unique file name for the file associated with the given key
|
||||
*/
|
||||
String getUniqueFileName(String key) {
|
||||
try {
|
||||
final MessageDigest md = MessageDigest.getInstance(MD5_ALGORITHM);
|
||||
return String.format("%032x", new BigInteger(1, md.digest(key.getBytes(StandardCharsets.UTF_8))));
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
// should not happen
|
||||
logger.error("Could not create MD5 hash for key '{}'", key, ex);
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="chromecast" 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>Chromecast Binding</name>
|
||||
<description>This is the binding for Google Chromecast devices.</description>
|
||||
<author>Kai Kreuzer</author>
|
||||
|
||||
<config-description>
|
||||
<parameter name="callbackUrl" type="text">
|
||||
<label>Callback URL</label>
|
||||
<description>url to use for playing notification sounds, e.g. http://192.168.0.2:8080</description>
|
||||
<required>false</required>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<config-description:config-descriptions
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0 https://openhab.org/schemas/config-description-1.0.0.xsd">
|
||||
|
||||
<config-description uri="thing-type:chromecast:device">
|
||||
<parameter name="ipAddress" type="text">
|
||||
<context>network-address</context>
|
||||
<label>Network Address</label>
|
||||
<description>Network address of the Chromecast device.</description>
|
||||
<required>true</required>
|
||||
</parameter>
|
||||
<parameter name="port" type="integer">
|
||||
<label>Network Port</label>
|
||||
<description>Network port of the Chromecast device.</description>
|
||||
<advanced>true</advanced>
|
||||
<default>8009</default>
|
||||
</parameter>
|
||||
<parameter name="refreshRate" type="integer">
|
||||
<label>Refresh Rate</label>
|
||||
<description>
|
||||
How often the chromecast should schedule a refresh. The chromecast should notify the binding when
|
||||
something changes, but if you want to track duration you'll need to schedule a refresh more often.
|
||||
</description>
|
||||
<advanced>true</advanced>
|
||||
<default>10</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</config-description:config-descriptions>
|
||||
@@ -0,0 +1,39 @@
|
||||
# binding
|
||||
binding.chromecast.name = Chromecast Binding
|
||||
binding.chromecast.description = Dieses Binding integriert Chromecast Geräte (z.B. Chromecast, Chromecast Audio oder Chromecast Ultra).
|
||||
|
||||
# thing types
|
||||
thing-type.chromecast.audiogroup.label = Chromecast Audiogruppe
|
||||
thing-type.chromecast.audiogroup.description = Audiogruppe aus mehreren Chromecast Audio oder Media Playern.
|
||||
|
||||
thing-type.chromecast.audio.label = Chromecast Audio
|
||||
thing-type.chromecast.audio.description = Chromecast Audio Player
|
||||
|
||||
thing-type.chromecast.chromecast.label = Chromecast
|
||||
thing-type.chromecast.chromecast.description = Chromecast Media Player
|
||||
|
||||
# thing types config
|
||||
thing-type.config.chromecast.device.ipAddress.label = IP-Adresse
|
||||
thing-type.config.chromecast.device.ipAddress.description = Lokale IP-Adresse oder Hostname des Chromecast Gerätes.
|
||||
thing-type.config.chromecast.device.port.label = Port
|
||||
thing-type.config.chromecast.device.port.description = Port des Chromecast Gerätes.
|
||||
thing-type.config.chromecast.device.refreshRate.label = Aktualisierungsintervall
|
||||
thing-type.config.chromecast.device.refreshRate.description = Intervall zur Aktualisierung des Chromecast Gerätes.
|
||||
|
||||
# channel types
|
||||
channel-type.chromecast.stop.label = Stop
|
||||
channel-type.chromecast.stop.description = Ermöglicht das Stoppen der Wiedergabe.
|
||||
channel-type.chromecast.playuri.label = URI abspielen
|
||||
channel-type.chromecast.playuri.description = Ermöglicht das Abspielen einer URI.
|
||||
channel-type.chromecast.metadataType.label = Medientyp
|
||||
channel-type.chromecast.metadataType.description = Zeigt den Medientyp des aktuellen Stücks oder Films (z. B. MOVIE, AUDIO_TRACK) an.
|
||||
channel-type.chromecast.albumName.label = Album
|
||||
channel-type.chromecast.albumName.description = Zeigt das Album des aktuellen Stücks an.
|
||||
channel-type.chromecast.metadataType.label = Medientyp
|
||||
channel-type.chromecast.metadataType.description = Zeigt den Medientyp des aktuellen Stücks oder Films (z. B. movie, song) an.
|
||||
channel-type.chromecast.image.label = Thumbnail
|
||||
channel-type.chromecast.image.description = Zeigt das Thumbnail des aktuellen Stücks oder Films an.
|
||||
channel-type.chromecast.currentTime.label = Laufzeit
|
||||
channel-type.chromecast.currentTime.description = Zeigt die Laufzeit des aktuellen Stücks oder Films an.
|
||||
channel-type.chromecast.duration.label = Dauer
|
||||
channel-type.chromecast.duration.description = Zeigt die Dauer des aktuellen Stücks oder Films an.
|
||||
@@ -0,0 +1,340 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="chromecast"
|
||||
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">
|
||||
|
||||
<!-- Chromecast Audio Group Thing Type -->
|
||||
<thing-type id="audiogroup">
|
||||
<label>Chromecast Audio Group</label>
|
||||
<description>A Google Chromecast Audio Group device</description>
|
||||
|
||||
<channels>
|
||||
<channel id="control" typeId="system.media-control"/>
|
||||
<channel id="stop" typeId="stop"/>
|
||||
<channel id="volume" typeId="system.volume"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="playuri" typeId="playuri"/>
|
||||
|
||||
<!-- App Information -->
|
||||
<channel id="appName" typeId="appName"/>
|
||||
<channel id="appId" typeId="appId"/>
|
||||
<channel id="idling" typeId="idling"/>
|
||||
<channel id="statustext" typeId="statustext"/>
|
||||
|
||||
<!-- Media Info -->
|
||||
<channel id="currentTime" typeId="currentTime"/>
|
||||
<channel id="duration" typeId="duration"/>
|
||||
|
||||
<!-- Metadata Info -->
|
||||
<channel id="metadataType" typeId="metadataType"/>
|
||||
<channel id="albumArtist" typeId="albumArtist"/>
|
||||
<channel id="albumName" typeId="albumName"/>
|
||||
<channel id="artist" typeId="system.media-artist"/>
|
||||
<channel id="broadcastDate" typeId="broadcastDate"/>
|
||||
<channel id="composer" typeId="composer"/>
|
||||
<channel id="creationDate" typeId="creationDate"/>
|
||||
<channel id="discNumber" typeId="discNumber"/>
|
||||
<channel id="episodeNumber" typeId="episodeNumber"/>
|
||||
<channel id="image" typeId="image"/>
|
||||
<channel id="imageSrc" typeId="imageSrc"/>
|
||||
<channel id="locationName" typeId="locationName"/>
|
||||
<channel id="location" typeId="system.location"/>
|
||||
<channel id="releaseDate" typeId="releaseDate"/>
|
||||
<channel id="seasonNumber" typeId="seasonNumber"/>
|
||||
<channel id="seriesTitle" typeId="seriesTitle"/>
|
||||
<channel id="studio" typeId="studio"/>
|
||||
<channel id="subtitle" typeId="subtitle"/>
|
||||
<channel id="title" typeId="system.media-title"/>
|
||||
<channel id="trackNumber" typeId="trackNumber"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>deviceId</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:chromecast:device"/>
|
||||
</thing-type>
|
||||
|
||||
<!-- Chromecast Audio Thing Type -->
|
||||
<thing-type id="audio">
|
||||
<label>Chromecast Audio</label>
|
||||
<description>A Google Chromecast Audio device</description>
|
||||
|
||||
<channels>
|
||||
<channel id="control" typeId="system.media-control"/>
|
||||
<channel id="stop" typeId="stop"/>
|
||||
<channel id="volume" typeId="system.volume"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="playuri" typeId="playuri"/>
|
||||
|
||||
<!-- App Information -->
|
||||
<channel id="appName" typeId="appName"/>
|
||||
<channel id="appId" typeId="appId"/>
|
||||
<channel id="idling" typeId="idling"/>
|
||||
<channel id="statustext" typeId="statustext"/>
|
||||
|
||||
<!-- Media Info -->
|
||||
<channel id="currentTime" typeId="currentTime"/>
|
||||
<channel id="duration" typeId="duration"/>
|
||||
|
||||
<!-- Metadata Info -->
|
||||
<channel id="metadataType" typeId="metadataType"/>
|
||||
<channel id="albumArtist" typeId="albumArtist"/>
|
||||
<channel id="albumName" typeId="albumName"/>
|
||||
<channel id="artist" typeId="system.media-artist"/>
|
||||
<channel id="broadcastDate" typeId="broadcastDate"/>
|
||||
<channel id="composer" typeId="composer"/>
|
||||
<channel id="creationDate" typeId="creationDate"/>
|
||||
<channel id="discNumber" typeId="discNumber"/>
|
||||
<channel id="episodeNumber" typeId="episodeNumber"/>
|
||||
<channel id="image" typeId="image"/>
|
||||
<channel id="imageSrc" typeId="imageSrc"/>
|
||||
<channel id="locationName" typeId="locationName"/>
|
||||
<channel id="location" typeId="system.location"/>
|
||||
<channel id="releaseDate" typeId="releaseDate"/>
|
||||
<channel id="seasonNumber" typeId="seasonNumber"/>
|
||||
<channel id="seriesTitle" typeId="seriesTitle"/>
|
||||
<channel id="studio" typeId="studio"/>
|
||||
<channel id="subtitle" typeId="subtitle"/>
|
||||
<channel id="title" typeId="system.media-title"/>
|
||||
<channel id="trackNumber" typeId="trackNumber"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>deviceId</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:chromecast:device"/>
|
||||
</thing-type>
|
||||
|
||||
<!-- Chromecast HDMI dongle Thing Type -->
|
||||
<thing-type id="chromecast">
|
||||
<label>Chromecast</label>
|
||||
<description>A Google Chromecast streaming device</description>
|
||||
|
||||
<channels>
|
||||
<channel id="control" typeId="system.media-control"/>
|
||||
<channel id="stop" typeId="stop"/>
|
||||
<channel id="volume" typeId="system.volume"/>
|
||||
<channel id="mute" typeId="system.mute"/>
|
||||
<channel id="playuri" typeId="playuri"/>
|
||||
|
||||
<!-- App Information -->
|
||||
<channel id="appName" typeId="appName"/>
|
||||
<channel id="appId" typeId="appId"/>
|
||||
<channel id="idling" typeId="idling"/>
|
||||
<channel id="statustext" typeId="statustext"/>
|
||||
|
||||
<!-- Media Info -->
|
||||
<channel id="currentTime" typeId="currentTime"/>
|
||||
<channel id="duration" typeId="duration"/>
|
||||
|
||||
<!-- Metadata Info -->
|
||||
<channel id="metadataType" typeId="metadataType"/>
|
||||
<channel id="albumArtist" typeId="albumArtist"/>
|
||||
<channel id="albumName" typeId="albumName"/>
|
||||
<channel id="artist" typeId="system.media-artist"/>
|
||||
<channel id="broadcastDate" typeId="broadcastDate"/>
|
||||
<channel id="composer" typeId="composer"/>
|
||||
<channel id="creationDate" typeId="creationDate"/>
|
||||
<channel id="discNumber" typeId="discNumber"/>
|
||||
<channel id="episodeNumber" typeId="episodeNumber"/>
|
||||
<channel id="image" typeId="image"/>
|
||||
<channel id="imageSrc" typeId="imageSrc"/>
|
||||
<channel id="locationName" typeId="locationName"/>
|
||||
<channel id="location" typeId="system.location"/>
|
||||
<channel id="releaseDate" typeId="releaseDate"/>
|
||||
<channel id="seasonNumber" typeId="seasonNumber"/>
|
||||
<channel id="seriesTitle" typeId="seriesTitle"/>
|
||||
<channel id="studio" typeId="studio"/>
|
||||
<channel id="subtitle" typeId="subtitle"/>
|
||||
<channel id="title" typeId="system.media-title"/>
|
||||
<channel id="trackNumber" typeId="trackNumber"/>
|
||||
</channels>
|
||||
|
||||
<representation-property>deviceId</representation-property>
|
||||
|
||||
<config-description-ref uri="thing-type:chromecast:device"/>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="stop">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Stop</label>
|
||||
<description>Stops the player. ON if the player is stopped.</description>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="playuri" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Play URI</label>
|
||||
<description>Plays a given URI</description>
|
||||
</channel-type>
|
||||
|
||||
<!--App Information -->
|
||||
<channel-type id="idling" advanced="true">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Idling</label>
|
||||
<description>Is Chromecast active or idling</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="appName" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>App</label>
|
||||
<description>Name of the currently running application</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="appId" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>App Id</label>
|
||||
<description>Id of the currently running application</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="statustext" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>App Status</label>
|
||||
<description>Status reported by the current application</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<!-- Media Information -->
|
||||
<channel-type id="currentTime" advanced="true">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Current Time</label>
|
||||
<description>Current time of currently playing media</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="duration" advanced="true">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Duration</label>
|
||||
<description>Length of currently playing media</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<!-- Metadata Information -->
|
||||
<channel-type id="metadataType" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Media Type</label>
|
||||
<description>The type of the currently playing media. One of GENERIC, MOVIE, TV_SHOW, AUDIO_TRACK, PHOTO</description>
|
||||
<state readOnly="true">
|
||||
<options>
|
||||
<option value="GENERIC"/>
|
||||
<option value="MOVIE"/>
|
||||
<option value="TV_SHOW"/>
|
||||
<option value="AUDIO_TRACK"/>
|
||||
<option value="PHOTO"/>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="albumArtist" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Album Artist</label>
|
||||
<description>The name of the album's artist</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="albumName" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Album Name</label>
|
||||
<description>The name of the album</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="broadcastDate" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Broadcast Date</label>
|
||||
<description>The broadcast date of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="composer" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Composer</label>
|
||||
<description>The composer of the current track</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="creationDate" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Creation Date</label>
|
||||
<description>The creation date of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="discNumber" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Disc Number</label>
|
||||
<description>The disc number of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="episodeNumber" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Episode Number</label>
|
||||
<description>The episode number of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="imageSrc" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Image URL</label>
|
||||
<description>The image URL that represents this media. Normally cover-art or scene from a movie</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="image" advanced="true">
|
||||
<item-type>Image</item-type>
|
||||
<label>Image</label>
|
||||
<description>The image that represents this media. Normally cover-art or scene from a movie</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="locationName" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Location Name</label>
|
||||
<description>The location of where the current media was taken</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="releaseDate" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Release Date</label>
|
||||
<description>The release date of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="seasonNumber" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Season Number</label>
|
||||
<description>The season number of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="seriesTitle" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Series Title</label>
|
||||
<description>The series title of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="studio" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Studio</label>
|
||||
<description>The studio of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="subtitle" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Subtitle</label>
|
||||
<description>The subtitle of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="trackNumber" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Track Number</label>
|
||||
<description>The track number of the currently playing media</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user