added migrated 2.x add-ons

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

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.chromecast</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,25 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
protobuf-java
* License: BSD License
* Project: https://developers.google.com/protocol-buffers
* Source: https://github.com/protocolbuffers/protobuf
chromecast-java-api-v2
* License: Apache 2.0 License
* Project: https://github.com/vitalidze/chromecast-java-api-v2
* Source: https://github.com/vitalidze/chromecast-java-api-v2

View File

@@ -0,0 +1,146 @@
# Chromecast Binding
The binding integrates Google Chromecast streaming devices.
It not only acts as a typical binding, but also registers each Chromecast device as an audio sink that can be used for playback.
When a Chromecast is used as an audio sink, the Chromecast connects to the runtime to get the audio streams.
The binding sends the Chromecast URLs for getting the audio streams based on the Primary Address (Network Settings configuration) and the runtime HTTP port.
These URL defaults can be overridden with the Callback URL configuration parameter.
This can be configured on the binding level:
| Configuration Parameter | Type | Description |
|-------------------------|------|----------------------------------------------------------------------------------------------------|
| callbackUrl | text | optional Callback URL - url to use for playing notification sounds, e.g. <http://192.168.0.2:8080> |
Configure a Callback URL when the Chromecast cannot connect using the Primary Address or Port, e.g. when:
* proxying HTTP (port 80/443) using Apache/NGINX to openHAB (port 8080)
* openHAB is running inside a Docker container that has its own IP Address
## Supported Things
| Things | Description | Thing Type |
|------------------|------------------------------------------------------------------------------|------------|
| Chromecast | Classic HDMI video Chromecasts and Google Homes | chromecast |
| Chromecast Audio | The Chromecast which only does audio streaming and offers a headphone jack | audio |
| Audio Group | A Chromecast audio group for multi-room audio defined via the Chromecast app | audiogroup |
## Discovery
Chromecast devices are discovered on the network using UPnP.
No authentication is required for accessing the devices on the network.
## Thing Configuration
Chromecast devices can also be manually added.
The only configuration parameter is the `ipAddress`.
For an audio group also the port is necessary.
The autodiscovery process finds the port automatically.
With manual thing configuration the parameter `port` must be determined manually.
Example for audio group:
```java
Thing chromecast:audiogroup:bathroom [ ipAddress="192.168.0.23", port=42139]
```
## Channels
| Channel Type ID | Item Type | Description |
|-----------------|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| control | Player | Player control; currently only supports play/pause and does not correctly update, if the state changes on the device itself |
| stop | Switch | Send `ON` to this channel: Stops the Chromecast. If this channel is `ON`, the Chromecast is stopped, otherwise it is in another state (see control channel) |
| volume | Dimmer | Control the volume, this is also updated if the volume is changed by another app |
| mute | Switch | Mute the audio |
| playuri | String | Can be used to tell the Chromecast to play media from a given url |
| appName | String | Name of currently running application |
| appId | String | ID of currently running application |
| idling | Switch | Read-only indication on weather Chromecast is on idle screen |
| statustext | String | |
| currentTime | Number:Time | Current time of currently playing media |
| duration | Number:Time | Duration of current track (null if between tracks) |
| metadataType | String | Type of metadata, this indicates what metadata may be available. One of: GenericMediaMetadata, MovieMediaMetadata, TvShowMediaMetadata, MusicTrackMediaMetadata, PhotoMediaMetadata. |
| subtitle | String | (GenericMediaMetadata) Descriptive subtitle of the content |
| title | String | (GenericMediaMetadata) Descriptive title of the content |
| image | Image | (GenericMediaMetadata) Image for current media |
| imageSrc | String | (GenericMediaMetadata) URL of image for current media |
| releaseDate | DateTime | (GenericMediaMetadata) ISO 8601 date and time this content was released |
| albumArtist | String | (MusicTrackMediaMetadata) Name of the artist associated with the album featuring this track |
| albumName | String | (MusicTrackMediaMetadata) Album or collection from which this track is drawn |
| artist | String | (MusicTrackMediaMetadata) Name of the artist associated with the media track |
| composer | String | (MusicTrackMediaMetadata) Name of the composer associated with the media track |
| discNumber | Number | (MusicTrackMediaMetadata) Number of the volume (for example, a disc) of the album |
| trackNumber | Number | (MusicTrackMediaMetadata) Number of the track on the album |
| creationDate | DateTime | (PhotoMediaMetadata) ISO 8601 date and time this photograph was taken |
| locationName | String | (PhotoMediaMetadata) Verbal location where the photograph was taken; for example, "Madrid, Spain." |
| location | Location | (PhotoMediaMetadata) Geographical location of where the photograph was taken |
| broadcastDate | DateTime | (TvShowMediaMetadata) ISO 8601 date and time this episode was released |
| episodeNumber | Number | (TvShowMediaMetadata) Episode number (in the season) of the t.v. show |
| seasonNumber | Number | (TvShowMediaMetadata) Season number of the t.v. show |
| seriesTitle | String | (TvShowMediaMetadata) Descriptive title of the t.v. series |
| studio | String | (TvShowMediaMetadata) Studio which released the content |
## Full Example
services.cfg:
```java
binding.chromecast:callbackUrl=http://192.168.30.58:8080
```
demo.things:
```java
Thing chromecast:audio:myCC "Lounge Chromecast Audio" [ipAddress="192.168.xxx.xxx", port=xxxx]
Thing chromecast:chromecast:KitchenHomeHub "Kitchen Home Hub" [ipAddress="192.168.xxx.xxx", port=8009]
```
demo.items:
```java
Dimmer Volume { channel="chromecast:audio:myCC:volume" }
Player Music { channel="chromecast:audio:myCC:control" }
```
demo.rules:
```javascript
rule "Turn on kitchen speakers when Chromecast starts playing music"
when
Item chromecast_chromecast_38e621581281c7675a777e7b474811ed_appId changed
then
logInfo("RULE.AUDIO", "Chromecast id changed!")
// 36061251 Pandora
// 2872939A Google Play Music
if (chromecast_chromecast_38e621581281c7675a777e7b474811ed_appId.state == "36061251"
|| chromecast_chromecast_38e621581281c7675a777e7b474811ed_appId.state == "2872939A") {
kitchen_audio_power.sendCommand(ON)
kitchen_audio_source.sendCommand(1)
}
end
```
demo.sitemap:
```perl
sitemap demo label="Main Menu" {
Frame {
Default item=Music
Slider item=Volume icon=soundvolume
}
}
```
```perl
sitemap chromecast label="Chromecasts" {
Frame label="Family Room: What's Playing" {
Image item=chromecast_chromecast_38e621581281c7675a777e7b474811ed_image
Text item=chromecast_chromecast_38e621581281c7675a777e7b474811ed_artist label="Artist [%s]"
Text item=chromecast_chromecast_38e621581281c7675a777e7b474811ed_title label="Title [%s]"
Text item=chromecast_chromecast_38e621581281c7675a777e7b474811ed_albumName label="Album [%s]"
}
}
```

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.chromecast</artifactId>
<name>openHAB Add-ons :: Bundles :: Chromecast Binding</name>
<properties>
<dep.noembedding>jackson-core,jackson-annotations,jackson-databind</dep.noembedding>
<jackson.version>2.9.10</jackson.version>
</properties>
<dependencies>
<dependency>
<groupId>su.litvak.chromecast</groupId>
<artifactId>api-v2</artifactId>
<version>0.11.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.6.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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