diff --git a/addons/binding/org.openhab.binding.kodi/.classpath b/addons/binding/org.openhab.binding.kodi/.classpath new file mode 100644 index 0000000000000..a95e0906ca013 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/.classpath @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/binding/org.openhab.binding.kodi/.project b/addons/binding/org.openhab.binding.kodi/.project new file mode 100644 index 0000000000000..f6815b33ca01f --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/.project @@ -0,0 +1,33 @@ + + + org.openhab.binding.kodi + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.pde.ds.core.builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + + diff --git a/addons/binding/org.openhab.binding.kodi/ESH-INF/binding/binding.xml b/addons/binding/org.openhab.binding.kodi/ESH-INF/binding/binding.xml new file mode 100644 index 0000000000000..2aac8c26a18d2 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/ESH-INF/binding/binding.xml @@ -0,0 +1,18 @@ + + + + Kodi Binding + This is the binding for Kodi. + Paul Frank + + + + + url to use for playing notification sounds, e.g. http://192.168.0.2:8080 + false + + + diff --git a/addons/binding/org.openhab.binding.kodi/ESH-INF/i18n/kodi_de.properties b/addons/binding/org.openhab.binding.kodi/ESH-INF/i18n/kodi_de.properties new file mode 100644 index 0000000000000..56904da9c8eeb --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/ESH-INF/i18n/kodi_de.properties @@ -0,0 +1,40 @@ +# binding +binding.kodi.name = Kodi Binding +binding.kodi.description = Das Kodi Binding erlaubt das Kodi Media Center zu steuern + +# thing types +thing-type.kodi.kodi.label = Kodi Media Center +thing-type.kodi.kodi.description = Kodi Media Center Binding +thing-type.config.kodi.kodi.ipAddress.label = Netzwerkaddresse +thing-type.config.kodi.kodi.ipAddress.description = IP Addresse oder Host Name des Kodi Media Centers +thing-type.config.kodi.kodi.port.label = Port +thing-type.config.kodi.kodi.port.description = Port des Web Socket Services des Kodi Media Centers + +# channel types +channel-type.kodi.volume.label = Lautstärke +channel-type.kodi.volume.description = Lautstärke des Kodi Media Centers +channel-type.kodi.mute.label = Stumm schalten +channel-type.kodi.mute.description = Das Kodi Media Centers stumm schalten +channel-type.kodi.control.label = Kontrolle +channel-type.kodi.control.description = Steuert das Kodi Media Center (z.B. Play, Pause, Stop) +channel-type.kodi.playuri.label = URI abspielen +channel-type.kodi.playuri.description = Spielt einen URI ab +channel-type.kodi.shownotification.label = Nachricht anzeigen +channel-type.kodi.shownotification.description = Zeigt eine Nachricht auf dem Bildschirm an +channel-type.kodi.input.label = Tastendruck +channel-type.kodi.input.description = Schickt einen Tastendruck an das Kodi Media Center um auf dem Bildschirm zu navigieren +channel-type.kodi.inputtext.label = Texteingabe +channel-type.kodi.inputtext.description = Schickt eine Zeichenkette als Eingabe an das Kodi Media Center +channel-type.kodi.systemcommand.label = System Kommando +channel-type.kodi.systemcommand.description = Schickt ein System Kommando zum Kodi Media Center um das Media Center neu zu starten, in den Ruhezustand oder Stromsparmodus zu versetzen, herunterzufahren +channel-type.kodi.title.label = Titel +channel-type.kodi.title.description = Titel des aktuellen Stücks oder Films +channel-type.kodi.showtitle.label = Fernseh Show Titel +channel-type.kodi.showtitle.description = Titel der aktuellen Fernseh Show +channel-type.kodi.album.label = Album +channel-type.kodi.album.description = Album des aktuellen Stücks +channel-type.kodi.artist.label = Künstler +channel-type.kodi.artist.description = Künstler des aktuellen Stücks oder Films +channel-type.kodi.mediatype.label = Medientyp +channel-type.kodi.mediatype.description = Medientyp des aktuellen Stücks oder Films (z. B. movie, song) + diff --git a/addons/binding/org.openhab.binding.kodi/ESH-INF/thing/thing-types.xml b/addons/binding/org.openhab.binding.kodi/ESH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..6e70940c77be6 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/ESH-INF/thing/thing-types.xml @@ -0,0 +1,144 @@ + + + + + + + Kodi Mediacenter Binding + + + + + + + + + + + + + + + + + + + unknown + + + + + + The IP or host name of the kodi + network-address + + + + Port for the web socket service + 9090 + + + + + + + Switch + + Mute/unmute your device + + + Dimmer + + Volume of your device + + + + + Player + + Control the Kodi Player, e.g. start/stop/next/previous/ffward/rewind + Player + + + String + + Play the given URI + + + String + + Shows a notification on the UI + + + String + + Sends a key stroke to the Kodi Player to navigate in the UI + + + + + + + + + + + + + + + + + + String + + Sends a text to the Kodi Player to simulate keyboard text entry + + + String + + Sends a system command to kodi. This allows to shutdown/suspend/hibernate/reboot kodi + + + + + + + + + + + + + String + + Title of the current song + + + + String + + Title of the current show + + + + String + + Album name of the current song + + + + String + + Artist name of the current song + + + + String + + Media type of the current file + + + diff --git a/addons/binding/org.openhab.binding.kodi/META-INF/MANIFEST.MF b/addons/binding/org.openhab.binding.kodi/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000..cbd0b6aab4d39 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/META-INF/MANIFEST.MF @@ -0,0 +1,39 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Kodi Binding +Bundle-SymbolicName: org.openhab.binding.kodi;singleton:=true +Bundle-Vendor: openHAB +Bundle-Version: 2.0.0.qualifier +Bundle-RequiredExecutionEnvironment: JavaSE-1.7 +Bundle-ClassPath: . +Import-Package: + com.google.common.base, + com.google.common.collect, + com.google.gson, + org.apache.commons.lang, + org.eclipse.jetty.util.component, + org.eclipse.jetty.websocket.api, + org.eclipse.jetty.websocket.api.annotations, + org.eclipse.jetty.websocket.client, + org.eclipse.jetty.websocket.common, + org.eclipse.smarthome.config.core, + org.eclipse.smarthome.config.discovery, + org.eclipse.smarthome.core.audio, + org.eclipse.smarthome.core.library.types, + org.eclipse.smarthome.core.net, + org.eclipse.smarthome.core.thing, + org.eclipse.smarthome.core.thing.binding, + org.eclipse.smarthome.core.thing.binding.builder, + org.eclipse.smarthome.core.thing.type, + org.eclipse.smarthome.core.types, + org.jupnp.model.meta, + org.jupnp.model.types, + org.openhab.binding.kodi, + org.openhab.binding.kodi.handler, + org.osgi.framework, + org.osgi.service.component, + org.slf4j +Service-Component: OSGI-INF/*.xml +Export-Package: org.openhab.binding.kodi, + org.openhab.binding.kodi.handler + diff --git a/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiDiscovery.xml b/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiDiscovery.xml new file mode 100644 index 0000000000000..f047d8899461d --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiDiscovery.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiHandlerFactory.xml b/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiHandlerFactory.xml new file mode 100644 index 0000000000000..1060ab1babb12 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/OSGI-INF/KodiHandlerFactory.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.kodi/README.md b/addons/binding/org.openhab.binding.kodi/README.md new file mode 100644 index 0000000000000..9e6a7df6ac8f2 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/README.md @@ -0,0 +1,80 @@ +# Kodi Binding + +[Kodi](https://kodi.tv) (formerly known as XBMC) is an free and open source (GPL) software media center for playing videos, music, pictures, games, and more. +Kodi runs on Linux, OS X, Windows, iOS, and Android. +It allows users to play and view most videos, music, podcasts, and other digital media files from local and network storage media and the internet. + +The Kodi Binding integrated Kodi media center support with openHAB, allowing both controlling the player as well as retrieving player status data like the currently played movie title. + +The Kodi binding is the successor to the openHAB 1.x xbmc binding. + +## Preparation + +In order to allow control of Kodi by this binding, you need to enable the Kodi application remote control feature. +Please enable "Allow remote control form applications" in the Kodi Setting menu under: + +* Settings âž” Services âž” Control âž” Allow remote control from applications on this/other systems + +## Supported Things + +This binding provides only one thing type: The Kodi media center. +Create one Kodi thing per Kodi instance available in your home automation system. + +All Kodi devices are registered as an audio sink in the ESH/openHAB 2 framework. + + +## Discovery + +The binding supports auto-discovery for available and prepared (see above) instances of the Kodi media center on your local network. +Auto-discovery is enabled by default. +To disable it, you can add the following line to `/services/runtime.cfg`: + +``` +org.openhab.kodi:enableAutoDiscovery=false +``` + +## Binding Configuration + +The following configuration options are available for the Kodi binding: + +| Parameter | Name | Description | Required | +|-----------|------|-------------|----------| +| `callbackUrl` | Callback URL | URL to use for playing notification sounds, e.g. `http://192.168.0.2:8080` | no | + + +## Thing Configuration + +The Kodi thing requires the IP address of the device hosting your Kodi media center instance and the TCP port to access it on (default: `9090`). +These parameters will be found by the auto-discovery feature. + +A manual setup through a `things` file could look like this: + +``` +Kodi:Kodi:myKodi [ipAddress="192.168.1.100", port="9090"] +``` + +## Channels + +The Kodi thing supports the following channels: + +| Channel Type ID | Item Type | Description | +|-------------------------|--------------|--------------| +| mute | Switch | Mute/unmute your playback | +| volume | Dimmer | Read or control the volume of your playback | +| control | Player | Control the Kodi player, e.g. play/pause/next/previous/ffward/rewind | +| title | String | Title of the currently played song/movie/tv episode | +| showtitle | String | Title of the currently played tv-show; empty for other types | +| album | String | Album name of the currently played song | +| artist | String | Artist name of the currently played song or director of the currently played movie| +| playuri | String | Plays the file with the provided URI | +| shownotification | String | Shows the provided notification message on the screen | +| input | String | Allows to control Kodi. Valid values are: `Up`, `Down`, `Left`, `Right`, `Select`, `Back`, `Home`, `ContextMenu`, `Info`, `ShowCodec`, `ShowOSD` | +| inputtext | String | This channel emulates a keyboard input | +| systemcommand | String | This channel allows to send commands to shutdown/suspend/hibernate/reboot kodi | +| mediatype | String | The media type of the current file. e.g. song or movie | + + +## Audio Support + +All supported Kodi instances are registered as an audio sink in the framework. +Audio streams are sent to the `playuri` channel. diff --git a/addons/binding/org.openhab.binding.kodi/build.properties b/addons/binding/org.openhab.binding.kodi/build.properties new file mode 100644 index 0000000000000..f4eebb897e877 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/build.properties @@ -0,0 +1,8 @@ +source.. = src/main/java/ +output.. = target/classes +bin.includes = META-INF/,\ + .,\ + OSGI-INF/,\ + ESH-INF/ + + diff --git a/addons/binding/org.openhab.binding.kodi/pom.xml b/addons/binding/org.openhab.binding.kodi/pom.xml new file mode 100644 index 0000000000000..aaeef8ad1c0eb --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/pom.xml @@ -0,0 +1,21 @@ + + + + 4.0.0 + + + + org.openhab.binding + pom + 2.0.0-SNAPSHOT + + + + org.openhab.binding + org.openhab.binding.kodi + 2.0.0-SNAPSHOT + + Kodi Binding + eclipse-plugin + diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/KodiBindingConstants.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/KodiBindingConstants.java new file mode 100644 index 0000000000000..ab0378c3f2641 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/KodiBindingConstants.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi; + +import java.util.Set; + +import org.eclipse.smarthome.core.thing.ThingTypeUID; + +import com.google.common.collect.ImmutableSet; + +/** + * The {@link KodiBinding} class defines common constants, which are + * used across the whole binding. + * + * @author Paul Frank - Initial contribution + */ +public class KodiBindingConstants { + + public static final String BINDING_ID = "kodi"; + + // List of all Thing Type UIDs + public final static ThingTypeUID THING_TYPE_KODI = new ThingTypeUID(BINDING_ID, "kodi"); + public final static Set SUPPORTED_THING_TYPES_UIDS = ImmutableSet.of(THING_TYPE_KODI); + + // List of thing parameters names + public final static String HOST_PARAMETER = "ipAddress"; + public final static String PORT_PARAMETER = "port"; + + // List of all Channel ids + public final static String CHANNEL_MUTE = "mute"; + public final static String CHANNEL_VOLUME = "volume"; + public final static String CHANNEL_CONTROL = "control"; + public final static String CHANNEL_PLAYURI = "playuri"; + public final static String CHANNEL_SHOWNOTIFICATION = "shownotification"; + + public final static String CHANNEL_INPUT = "input"; + public final static String CHANNEL_INPUTTEXT = "inputtext"; + public final static String CHANNEL_SYSTEMCOMMAND = "systemcommand"; + + public final static String CHANNEL_ARTIST = "artist"; + public final static String CHANNEL_TITLE = "title"; + public final static String CHANNEL_SHOWTITLE = "showtitle"; + public final static String CHANNEL_ALBUM = "album"; + public final static String CHANNEL_MEDIATYPE = "mediatype"; + + // Module Properties + public final static String PROPERTY_VERSION = "version"; + + // Used for Discovery service + public final static String MANUFACTURER = "XBMC Foundation"; + public final static String UPNP_DEVICE_TYPE = "MediaRenderer"; + +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/handler/KodiHandler.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/handler/KodiHandler.java new file mode 100644 index 0000000000000..bd699f2ab04af --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/handler/KodiHandler.java @@ -0,0 +1,324 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.handler; + +import static org.openhab.binding.kodi.KodiBindingConstants.*; + +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.smarthome.core.library.types.IncreaseDecreaseType; +import org.eclipse.smarthome.core.library.types.NextPreviousType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.PlayPauseType; +import org.eclipse.smarthome.core.library.types.RewindFastforwardType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandler; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.eclipse.smarthome.core.types.UnDefType; +import org.openhab.binding.kodi.internal.KodiEventListener; +import org.openhab.binding.kodi.internal.protocol.KodiConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link KodiHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Paul Frank - Initial contribution + */ +public class KodiHandler extends BaseThingHandler implements KodiEventListener { + + private Logger logger = LoggerFactory.getLogger(KodiHandler.class); + + private final KodiConnection connection; + + private ScheduledFuture connectionCheckerFuture; + + public KodiHandler(Thing thing) { + super(thing); + connection = new KodiConnection(this); + } + + @Override + public void dispose() { + super.dispose(); + if (connectionCheckerFuture != null) { + connectionCheckerFuture.cancel(true); + } + if (connection != null) { + connection.close(); + } + } + + private int getIntConfigParameter(String key, int defaultValue) { + Object obj = this.getConfig().get(key); + if (obj instanceof Number) { + return ((Number) obj).intValue(); + } else if (obj instanceof String) { + return Integer.parseInt(obj.toString()); + } + return defaultValue; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + switch (channelUID.getIdWithoutGroup()) { + case CHANNEL_MUTE: + if (command.equals(OnOffType.ON)) { + connection.setMute(true); + } else if (command.equals(OnOffType.OFF)) { + connection.setMute(false); + } else if (command.equals(RefreshType.REFRESH)) { + connection.updateVolume(); + } + break; + case CHANNEL_VOLUME: + if (command instanceof PercentType) { + connection.setVolume(((PercentType) command).intValue()); + } else if (command.equals(IncreaseDecreaseType.INCREASE)) { + connection.increaseVolume(); + } else if (command.equals(IncreaseDecreaseType.DECREASE)) { + connection.decreaseVolume(); + } else if (command.equals(OnOffType.OFF)) { + connection.setVolume(0); + } else if (command.equals(OnOffType.ON)) { + connection.setVolume(100); + } else if (command.equals(RefreshType.REFRESH)) { + connection.updateVolume(); + } + break; + case CHANNEL_CONTROL: + if (command instanceof PlayPauseType) { + if (command.equals(PlayPauseType.PLAY)) { + connection.playerPlayPause(); + } else if (command.equals(PlayPauseType.PAUSE)) { + connection.playerPlayPause(); + } + } else if (command instanceof NextPreviousType) { + if (command.equals(NextPreviousType.NEXT)) { + connection.playerNext(); + } else if (command.equals(NextPreviousType.PREVIOUS)) { + connection.playerPrevious(); + } + } else if (command instanceof RewindFastforwardType) { + if (command.equals(RewindFastforwardType.REWIND)) { + connection.playerRewind(); + } else if (command.equals(RewindFastforwardType.FASTFORWARD)) { + connection.playerFastForward(); + } + } else if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + case CHANNEL_PLAYURI: + if (command instanceof StringType) { + playURI(command); + } else if (command.equals(RefreshType.REFRESH)) { + // updateState(CHANNEL_PLAYURI, new StringType("")); + } + break; + case CHANNEL_SHOWNOTIFICATION: + if (command instanceof StringType) { + connection.showNotification(command.toString()); + } else if (command.equals(RefreshType.REFRESH)) { + // updateState(CHANNEL_SHOWNOTIFICATION, new StringType("")); + } + break; + case CHANNEL_INPUT: + if (command instanceof StringType) { + connection.input(command.toString()); + updateState(CHANNEL_INPUT, UnDefType.UNDEF); + } else if (command.equals(RefreshType.REFRESH)) { + updateState(CHANNEL_INPUT, UnDefType.UNDEF); + } + break; + case CHANNEL_INPUTTEXT: + if (command instanceof StringType) { + connection.inputText(command.toString()); + updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF); + } else if (command.equals(RefreshType.REFRESH)) { + updateState(CHANNEL_INPUTTEXT, UnDefType.UNDEF); + } + break; + case CHANNEL_SYSTEMCOMMAND: + if (command instanceof StringType) { + connection.sendSystemCommand(command.toString()); + updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF); + } else if (command.equals(RefreshType.REFRESH)) { + updateState(CHANNEL_SYSTEMCOMMAND, UnDefType.UNDEF); + } + break; + case CHANNEL_ARTIST: + if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + case CHANNEL_ALBUM: + if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + case CHANNEL_TITLE: + if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + case CHANNEL_SHOWTITLE: + if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + case CHANNEL_MEDIATYPE: + if (command.equals(RefreshType.REFRESH)) { + connection.updatePlayerStatus(); + } + break; + default: + logger.debug("Received unknown channel {}", channelUID.getIdWithoutGroup()); + break; + } + + } + + public void playURI(Command command) { + connection.playURI(command.toString()); + } + + public void playNotificationSoundURI(Command command) { + connection.playNotificationSoundURI(command.toString()); + } + + public PercentType getNotificationSoundVolume() { + return new PercentType(connection.getVolume()); + } + + public void setNotificationSoundVolume(PercentType volume) { + connection.setVolume(volume.intValue()); + } + + @Override + public void initialize() { + try { + String host = this.getConfig().get(HOST_PARAMETER).toString(); + if (host == null || host.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "No network address specified"); + } else { + connection.connect(host, getIntConfigParameter(PORT_PARAMETER, 9090), scheduler); + + // Start the connection checker + Runnable connectionChecker = new Runnable() { + @Override + public void run() { + try { + if (!connection.checkConnection()) { + updateStatus(ThingStatus.OFFLINE); + } + } catch (Exception ex) { + logger.warn("Exception in check connection to @{}. Cause: {}", + connection.getConnectionName(), ex.getMessage()); + + } + } + }; + connectionCheckerFuture = scheduler.scheduleWithFixedDelay(connectionChecker, 1, 10, TimeUnit.SECONDS); + } + } catch (Exception e) { + logger.debug("error during opening connection: {}", e.getMessage()); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + @Override + public void updateConnectionState(boolean connected) { + if (connected) { + updateStatus(ThingStatus.ONLINE); + try { + String version = connection.getVersion(); + thing.setProperty(PROPERTY_VERSION, version); + } catch (Exception e) { + logger.error("error during reading version: {}", e.getMessage()); + } + } else { + updateStatus(ThingStatus.OFFLINE); + } + } + + @Override + public void updateScreenSaverState(boolean screenSaveActive) { + + } + + @Override + public void updateVolume(int volume) { + updateState(CHANNEL_VOLUME, new PercentType(volume)); + } + + @Override + public void updatePlayerState(KodiState state) { + switch (state) { + case Play: + updateState(CHANNEL_CONTROL, PlayPauseType.PLAY); + break; + case Pause: + case Stop: + case End: + updateState(CHANNEL_CONTROL, PlayPauseType.PAUSE); + break; + case FastForward: + updateState(CHANNEL_CONTROL, RewindFastforwardType.FASTFORWARD); + break; + case Rewind: + updateState(CHANNEL_CONTROL, RewindFastforwardType.REWIND); + break; + } + } + + @Override + public void updateMuted(boolean muted) { + if (muted) { + updateState(CHANNEL_MUTE, OnOffType.ON); + } else { + updateState(CHANNEL_MUTE, OnOffType.OFF); + } + } + + @Override + public void updateTitle(String title) { + updateState(CHANNEL_TITLE, new StringType(title)); + } + + @Override + public void updateShowTitle(String title) { + updateState(CHANNEL_SHOWTITLE, new StringType(title)); + } + + @Override + public void updateAlbum(String album) { + updateState(CHANNEL_ALBUM, new StringType(album)); + } + + @Override + public void updateArtist(String artist) { + updateState(CHANNEL_ARTIST, new StringType(artist)); + } + + @Override + public void updateMediaType(String mediaType) { + updateState(CHANNEL_MEDIATYPE, new StringType(mediaType)); + } + +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java new file mode 100644 index 0000000000000..caf4735d95a33 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiAudioSink.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal; + +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import org.eclipse.smarthome.core.audio.AudioFormat; +import org.eclipse.smarthome.core.audio.AudioHTTPServer; +import org.eclipse.smarthome.core.audio.AudioSink; +import org.eclipse.smarthome.core.audio.AudioStream; +import org.eclipse.smarthome.core.audio.URLAudioStream; +import org.eclipse.smarthome.core.audio.UnsupportedAudioFormatException; +import org.eclipse.smarthome.core.library.types.PercentType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.openhab.binding.kodi.handler.KodiHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This makes kodi to serve as an {@link AudioSink}- + * + * @author Kai Kreuzer - Initial contribution and API + * @author Paul Frank - Adapted for kodi + * + */ +public class KodiAudioSink implements AudioSink { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private static HashSet supportedFormats = new HashSet<>(); + + static { + supportedFormats.add(AudioFormat.WAV); + supportedFormats.add(AudioFormat.MP3); + } + + private AudioHTTPServer audioHTTPServer; + private KodiHandler handler; + private final String callbackUrl; + + public KodiAudioSink(KodiHandler handler, AudioHTTPServer audioHTTPServer, String callbackUrl) { + this.handler = handler; + this.audioHTTPServer = audioHTTPServer; + this.callbackUrl = callbackUrl; + } + + @Override + public String getId() { + return handler.getThing().getUID().toString(); + } + + @Override + public String getLabel(Locale locale) { + return handler.getThing().getLabel(); + } + + @Override + public void process(AudioStream audioStream) throws UnsupportedAudioFormatException { + String url = null; + 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 = audioHTTPServer.serve(audioStream); + url = callbackUrl + relativeUrl; + } else { + logger.warn("We do not have any callback url, so kodi cannot play the audio stream!"); + return; + } + } + handler.playURI(new StringType(url)); + + } + + @Override + public Set getSupportedFormats() { + return supportedFormats; + } + + @Override + public PercentType getVolume() { + return handler.getNotificationSoundVolume(); + } + + @Override + public void setVolume(PercentType volume) { + handler.setNotificationSoundVolume(volume); + } + +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiEventListener.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiEventListener.java new file mode 100644 index 0000000000000..d0543c3a72a9f --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiEventListener.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal; + +import java.util.EventListener; + +import org.openhab.binding.kodi.internal.protocol.KodiConnection; + +/** + * Interface which has to be implemented by a class in order to get status updates from a {@link KodiConnection} + * + * @author Paul Frank + * + */ +public interface KodiEventListener extends EventListener { + public enum KodiState { + Play, + Pause, + End, + Stop, + Rewind, + FastForward + } + + void updateConnectionState(boolean connected); + + void updateScreenSaverState(boolean screenSaveActive); + + void updateVolume(int volume); + + void updatePlayerState(KodiState state); + + void updateMuted(boolean muted); + + void updateTitle(String title); + + void updateShowTitle(String title); + + void updateAlbum(String album); + + void updateArtist(String artist); + + void updateMediaType(String mediaType); +} \ No newline at end of file diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java new file mode 100644 index 0000000000000..609431067d16c --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/KodiHandlerFactory.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal; + +import static org.openhab.binding.kodi.KodiBindingConstants.*; + +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.eclipse.smarthome.core.audio.AudioHTTPServer; +import org.eclipse.smarthome.core.audio.AudioSink; +import org.eclipse.smarthome.core.net.HttpServiceUtil; +import org.eclipse.smarthome.core.net.NetUtil; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory; +import org.eclipse.smarthome.core.thing.binding.ThingHandler; +import org.openhab.binding.kodi.handler.KodiHandler; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link KodiHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Paul Frank - Initial contribution + */ +public class KodiHandlerFactory extends BaseThingHandlerFactory { + + private Logger logger = LoggerFactory.getLogger(KodiHandlerFactory.class); + + private AudioHTTPServer audioHTTPServer; + + // url (scheme+server+port) to use for playing notification sounds + private String callbackUrl = null; + + private Map> audioSinkRegistrations = new ConcurrentHashMap<>(); + + @Override + protected void activate(ComponentContext componentContext) { + super.activate(componentContext); + Dictionary properties = componentContext.getProperties(); + callbackUrl = (String) properties.get("callbackUrl"); + }; + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (thingTypeUID.equals(THING_TYPE_KODI)) { + KodiHandler handler = new KodiHandler(thing); + + // register the kodi as an audio sink + KodiAudioSink audioSink = new KodiAudioSink(handler, audioHTTPServer, createCallbackUrl()); + @SuppressWarnings("unchecked") + ServiceRegistration reg = (ServiceRegistration) bundleContext + .registerService(AudioSink.class.getName(), audioSink, new Hashtable()); + audioSinkRegistrations.put(thing.getUID().toString(), reg); + + return handler; + } + + return null; + } + + private String createCallbackUrl() { + if (callbackUrl != null) { + return callbackUrl; + } else { + final String ipAddress = NetUtil.getLocalIpv4HostAddress(); + 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 reg = audioSinkRegistrations.get(thing.getUID().toString()); + if (reg != null) { + reg.unregister(); + } + } + + protected void setAudioHTTPServer(AudioHTTPServer audioHTTPServer) { + this.audioHTTPServer = audioHTTPServer; + } + + protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) { + this.audioHTTPServer = null; + } + +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/discovery/KodiDiscoveryParticipant.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/discovery/KodiDiscoveryParticipant.java new file mode 100644 index 0000000000000..22d72098e8911 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/discovery/KodiDiscoveryParticipant.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal.discovery; + +import static org.openhab.binding.kodi.KodiBindingConstants.*; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang.StringUtils; +import org.eclipse.smarthome.config.discovery.DiscoveryResult; +import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder; +import org.eclipse.smarthome.config.discovery.UpnpDiscoveryParticipant; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.thing.ThingUID; +import org.jupnp.model.meta.RemoteDevice; +import org.osgi.service.component.ComponentContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * An UpnpDiscoveryParticipant which allows to discover Kodi AVRs. + * + * @author Paul Frank + * + */ +public class KodiDiscoveryParticipant implements UpnpDiscoveryParticipant { + + private Logger logger = LoggerFactory.getLogger(KodiDiscoveryParticipant.class); + + private boolean isAutoDiscoveryEnabled; + private Set supportedThingTypes; + + public KodiDiscoveryParticipant() { + this.isAutoDiscoveryEnabled = true; + this.supportedThingTypes = SUPPORTED_THING_TYPES_UIDS; + } + + /** + * Called at the service activation. + * + * @param componentContext + */ + protected void activate(ComponentContext componentContext) { + if (componentContext.getProperties() != null) { + String autoDiscoveryPropertyValue = (String) componentContext.getProperties().get("enableAutoDiscovery"); + if (StringUtils.isNotEmpty(autoDiscoveryPropertyValue)) { + isAutoDiscoveryEnabled = Boolean.valueOf(autoDiscoveryPropertyValue); + } + } + supportedThingTypes = isAutoDiscoveryEnabled ? SUPPORTED_THING_TYPES_UIDS : new HashSet(); + } + + @Override + public Set getSupportedThingTypeUIDs() { + return supportedThingTypes; + } + + @Override + public DiscoveryResult createResult(RemoteDevice device) { + DiscoveryResult result = null; + ThingUID thingUid = getThingUID(device); + if (thingUid != null) { + + String label = StringUtils.isEmpty(device.getDetails().getFriendlyName()) ? device.getDisplayString() + : device.getDetails().getFriendlyName(); + Map properties = new HashMap<>(2, 1); + properties.put(HOST_PARAMETER, device.getIdentity().getDescriptorURL().getHost()); + + result = DiscoveryResultBuilder.create(thingUid).withLabel(label).withProperties(properties).build(); + } + + return result; + } + + @Override + public ThingUID getThingUID(RemoteDevice device) { + ThingUID result = null; + if (isAutoDiscoveryEnabled) { + String manufacturer = device.getDetails().getManufacturerDetails().getManufacturer(); + if (StringUtils.containsIgnoreCase(manufacturer, MANUFACTURER)) { + logger.debug("Manufacturer matched: search: {}, device value: {}.", MANUFACTURER, + device.getDetails().getManufacturerDetails().getManufacturer()); + if (StringUtils.containsIgnoreCase(device.getType().getType(), UPNP_DEVICE_TYPE)) { + logger.debug("Device type matched: search: {}, device value: {}.", UPNP_DEVICE_TYPE, + device.getType().getType()); + + ThingTypeUID thingTypeUID = THING_TYPE_KODI; + + result = new ThingUID(thingTypeUID, device.getIdentity().getUdn().getIdentifierString()); + } + } + } + + return result; + } + +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java new file mode 100644 index 0000000000000..d4ece6c27542c --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocket.java @@ -0,0 +1,235 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal.protocol; + +import java.net.URI; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; +import org.eclipse.jetty.websocket.api.annotations.WebSocket; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +/** + * KodiClientSocket implements the low level communication to kodi through websocket. Usually this communication is done + * through port 9090 + * + * @author Paul Frank + * + */ +public class KodiClientSocket { + private static final Logger logger = LoggerFactory.getLogger(KodiClientSocket.class); + + private final ScheduledExecutorService scheduler; + private static final int REQUEST_TIMEOUT_MS = 60000; + + private CountDownLatch commandLatch = null; + private JsonObject commandResponse = null; + private int nextMessageId = 1; + + private boolean connected = false; + + private final JsonParser parser = new JsonParser(); + private final Gson mapper = new Gson(); + private URI uri; + private Session session; + private WebSocketClient client; + + private final KodiClientSocketEventListener eventHandler; + + public KodiClientSocket(KodiClientSocketEventListener eventHandler, URI uri, ScheduledExecutorService scheduler) { + this.eventHandler = eventHandler; + this.uri = uri; + client = new WebSocketClient(); + this.scheduler = scheduler; + } + + /** + * Attempts to create a connection to the kodi host and begin listening + * for updates over the async http web socket + * + * @throws Exception + */ + public synchronized void open() throws Exception { + if (isConnected()) { + logger.warn("connect: connection is already open"); + } + if (!client.isStarted()) { + client.start(); + } + KodiWebSocketListener socket = new KodiWebSocketListener(); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + + client.connect(socket, uri, request); + } + + /*** + * Close this connection to the kodi instance + */ + public void close() { + // if there is an old web socket then clean up and destroy + if (session != null) { + try { + session.close(); + } catch (Exception e) { + logger.error("Exception during closing the websocket {}", e.getMessage(), e); + } + session = null; + } + try { + client.stop(); + } catch (Exception e) { + logger.error("Exception during closing the websocket {}", e.getMessage(), e); + } + } + + public boolean isConnected() { + if (session == null || !session.isOpen()) { + return false; + } + + return connected; + } + + @WebSocket + public class KodiWebSocketListener { + + @OnWebSocketConnect + public void onConnect(Session wssession) { + logger.debug("Connected to server"); + session = wssession; + connected = true; + if (eventHandler != null) { + scheduler.submit(new Runnable() { + + @Override + public void run() { + try { + eventHandler.onConnectionOpened(); + } catch (Exception e) { + logger.error("Error handling onConnectionOpened() {}", e.getMessage(), e); + } + + } + }); + + } + } + + @OnWebSocketMessage + public void onMessage(String message) { + logger.debug("Message received from server: {}", message); + final JsonObject json = parser.parse(message).getAsJsonObject(); + if (json.has("id")) { + logger.debug("Response received from server:" + json.toString()); + int messageId = json.get("id").getAsInt(); + if (messageId == nextMessageId - 1) { + commandResponse = json; + commandLatch.countDown(); + } + } else { + logger.debug("Event received from server: {}", json.toString()); + try { + if (eventHandler != null) { + scheduler.submit(new Runnable() { + + @Override + public void run() { + try { + eventHandler.handleEvent(json); + } catch (Exception e) { + logger.error("Error handling event {} player state change message: {}", json, + e.getMessage(), e); + } + + } + }); + + } + } catch (Exception e) { + logger.error("Error handling player state change message", e); + } + } + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + session = null; + connected = false; + logger.debug("Closing a WebSocket due to {}", reason); + scheduler.submit(new Runnable() { + + @Override + public void run() { + try { + eventHandler.onConnectionClosed(); + } catch (Exception e) { + logger.error("Error handling onConnectionClosed()", e); + } + } + }); + } + } + + private void sendMessage(String str) throws Exception { + if (isConnected()) { + logger.debug("send message: {}", str); + session.getRemote().sendString(str); + } else { + throw new Exception("socket not initialized"); + } + } + + public JsonElement callMethod(String methodName) { + return callMethod(methodName, null); + } + + public synchronized JsonElement callMethod(String methodName, JsonObject params) { + try { + JsonObject payloadObject = new JsonObject(); + payloadObject.addProperty("jsonrpc", "2.0"); + payloadObject.addProperty("id", nextMessageId); + payloadObject.addProperty("method", methodName); + + if (params != null) { + payloadObject.add("params", params); + } + + String message = mapper.toJson(payloadObject); + + commandLatch = new CountDownLatch(1); + commandResponse = null; + nextMessageId++; + + sendMessage(message); + if (commandLatch.await(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + logger.debug("callMethod returns {}", commandResponse.toString()); + return commandResponse.get("result"); + } else { + logger.error("Timeout during callMethod({}, {})", methodName, params != null ? params.toString() : ""); + return null; + } + } catch (Exception e) { + logger.error("Error during callMethod", e); + return null; + } + } +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocketEventListener.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocketEventListener.java new file mode 100644 index 0000000000000..1cf79be6b5783 --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiClientSocketEventListener.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal.protocol; + +import com.google.gson.JsonObject; + +/** + * This interface has to be implemented for classes which need to be able to receive events from KodiClientSocket + * + * @author Paul Frank + * + */ +public interface KodiClientSocketEventListener { + + void handleEvent(JsonObject json); + + void onConnectionClosed(); + + void onConnectionOpened(); + +} diff --git a/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiConnection.java b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiConnection.java new file mode 100644 index 0000000000000..86530ede85cda --- /dev/null +++ b/addons/binding/org.openhab.binding.kodi/src/main/java/org/openhab/binding/kodi/internal/protocol/KodiConnection.java @@ -0,0 +1,556 @@ +/** + * Copyright (c) 2014-2016 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.openhab.binding.kodi.internal.protocol; + +import java.net.URI; +import java.util.concurrent.ScheduledExecutorService; + +import org.openhab.binding.kodi.internal.KodiEventListener; +import org.openhab.binding.kodi.internal.KodiEventListener.KodiState; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * KodiConnection provides an api for accessing a kodi device. + * + * @author Paul Frank + * + */ +public class KodiConnection implements KodiClientSocketEventListener { + + private static final Logger logger = LoggerFactory.getLogger(KodiConnection.class); + + private static final int VOLUMESTEP = 10; + + private URI wsUri; + private KodiClientSocket socket; + + private int volume = 0; + private KodiState currentState = KodiState.Stop; + + private final KodiEventListener listener; + + public KodiConnection(KodiEventListener listener) { + this.listener = listener; + } + + @Override + public synchronized void onConnectionClosed() { + listener.updateConnectionState(false); + } + + @Override + public synchronized void onConnectionOpened() { + listener.updateConnectionState(true); + } + + public synchronized void connect(String hostName, int port, ScheduledExecutorService scheduler) { + try { + wsUri = new URI(String.format("ws://%s:%d/jsonrpc", hostName, port)); + socket = new KodiClientSocket(this, wsUri, scheduler); + socket.open(); + } catch (Throwable t) { + logger.error("exception during connect to {}", wsUri.toString(), t); + } + } + + private int getActivePlayer() { + JsonElement response = socket.callMethod("Player.GetActivePlayers"); + if (response != null) { + boolean playing = response.isJsonArray() && response.getAsJsonArray().size() > 0; + if (playing) { + JsonObject player0 = response.getAsJsonArray().get(0).getAsJsonObject(); + return player0.get("playerid").getAsInt(); + } + } + return -1; + } + + public synchronized void playerPlayPause() { + int activePlayer = getActivePlayer(); + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + socket.callMethod("Player.PlayPause", params); + } + + public synchronized void playerStop() { + int activePlayer = getActivePlayer(); + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + socket.callMethod("Player.Stop", params); + } + + public synchronized void playerNext() { + int activePlayer = getActivePlayer(); + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.addProperty("to", "next"); + socket.callMethod("Player.GoTo", params); + + updatePlayerStatus(); + } + + public synchronized void playerPrevious() { + int activePlayer = getActivePlayer(); + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.addProperty("to", "previous"); + socket.callMethod("Player.GoTo", params); + + updatePlayerStatus(); + } + + public synchronized void playerRewind() { + int activePlayer = getActivePlayer(); + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.addProperty("speed", "-1"); + socket.callMethod("Player.SetSpeed", params); + + updatePlayerStatus(); + + } + + public synchronized void playerFastForward() { + int activePlayer = getActivePlayer(); + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.addProperty("speed", "2"); + socket.callMethod("Player.SetSpeed", params); + + updatePlayerStatus(); + } + + public synchronized void increaseVolume() { + this.volume += VOLUMESTEP; + JsonObject params = new JsonObject(); + params.addProperty("volume", volume); + socket.callMethod("Application.SetVolume", params); + } + + public synchronized void decreaseVolume() { + this.volume -= VOLUMESTEP; + JsonObject params = new JsonObject(); + params.addProperty("volume", volume); + socket.callMethod("Application.SetVolume", params); + } + + public synchronized void setVolume(int volume) { + this.volume = volume; + JsonObject params = new JsonObject(); + params.addProperty("volume", volume); + socket.callMethod("Application.SetVolume", params); + } + + public int getVolume() { + return volume; + } + + public synchronized void setMute(boolean mute) { + JsonObject params = new JsonObject(); + params.addProperty("mute", mute); + socket.callMethod("Application.SetMute", params); + } + + private int getSpeed(int activePlayer) { + final String[] properties = { "speed", "position" }; + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.add("properties", getJsonArray(properties)); + JsonElement response = socket.callMethod("Player.GetProperties", params); + + JsonObject result = response.getAsJsonObject(); + return result.get("speed").getAsInt(); + } + + public synchronized void updatePlayerStatus() { + if (socket.isConnected()) { + int activePlayer = getActivePlayer(); + if (activePlayer >= 0) { + int speed = getSpeed(activePlayer); + if (speed == 0) { + updateState(KodiState.Stop); + } else if (speed == 1) { + updateState(KodiState.Play); + } else if (speed < 0) { + updateState(KodiState.Rewind); + } else { + updateState(KodiState.FastForward); + } + requestPlayerUpdate(activePlayer); + } else { + updateState(KodiState.Stop); + } + } + } + + private void requestPlayerUpdate(int activePlayer) { + requestPlayerUpdate(activePlayer, true); + } + + private void updateFanartUrl(String imagePath) { + if (imagePath == null || imagePath.isEmpty()) { + return; + } + + /* + * try { + * + * String encodedURL = URLEncoder.encode(imagePath, "UTF-8"); + * String decodedURL = URLDecoder.decode(imagePath, "UTF-8"); + * + * JsonObject params = new JsonObject(); + * params.addProperty("path", ""); + * JsonElement response = socket.callMethod("Files.PrepareDownload", params); + * + * } catch (Exception e) { + * logger.error("updateFanartUrl error", e); + * } + */ + } + + private void requestPlayerUpdate(int activePlayer, boolean updateMediaType) { + final String[] properties = { "title", "album", "artist", "director", "thumbnail", "file", "fanart", + "showtitle", "streamdetails" }; + + JsonObject params = new JsonObject(); + params.addProperty("playerid", activePlayer); + params.add("properties", getJsonArray(properties)); + JsonElement response = socket.callMethod("Player.GetItem", params); + + JsonObject item = ((JsonObject) response).get("item").getAsJsonObject(); + + String title = ""; + if (item.has("title")) { + title = convertToText(item.get("title")); + } + + String showTitle = ""; + if (item.has("showtitle")) { + showTitle = convertToText(item.get("showtitle")); + } + + String album = ""; + if (item.has("album")) { + album = convertToText(item.get("album")); + } + + String mediaType = item.get("type").getAsString(); + + String artist = ""; + if (mediaType.equals("movie")) { + + artist = convertFromArray(item.get("director").getAsJsonArray()); + } else { + if (item.has("artist")) { + artist = convertFromArray(item.get("artist").getAsJsonArray()); + } + } + + try { + listener.updateAlbum(album); + listener.updateTitle(title); + listener.updateShowTitle(showTitle); + listener.updateArtist(artist); + if (updateMediaType) { + listener.updateMediaType(mediaType); + } + } catch (Exception e) { + logger.error("Event listener invoking error", e); + } + + if (item.has("fanart")) { + updateFanartUrl(item.get("fanart").getAsString()); + } + } + + private JsonArray getJsonArray(String[] values) { + JsonArray result = new JsonArray(); + for (String param : values) { + result.add(new JsonPrimitive(param)); + } + return result; + } + + private String convertFromArray(JsonArray data) { + StringBuilder result = new StringBuilder(); + for (JsonElement x : data) { + if (result.length() > 0) { + result.append(", "); + } + result.append(convertToText(x)); + } + return result.toString(); + } + + private String convertToText(JsonElement element) { + String text = element.getAsString(); + return text; + // try { + // return new String(text.getBytes("ISO-8859-1")); + // } catch (UnsupportedEncodingException e) { + // return text; + // } + } + + private void updateState(KodiState state) { + // sometimes get a Pause immediately after a Stop - so just ignore + if (currentState.equals(KodiState.Stop) && state.equals(KodiState.Pause)) { + return; + } + try { + listener.updatePlayerState(state); + // if this is a Stop then clear everything else + if (state == KodiState.Stop) { + listener.updateAlbum(""); + listener.updateArtist(""); + listener.updateTitle(""); + listener.updateMediaType(""); + } + } catch (Exception e) { + logger.error("Event listener invoking error", e); + } + + // keep track of our current state + currentState = state; + } + + @Override + public void handleEvent(JsonObject json) { + JsonElement methodElement = json.get("method"); + + if (methodElement != null) { + String method = methodElement.getAsString(); + JsonObject params = json.get("params").getAsJsonObject(); + if (method.startsWith("Player.On")) { + processPlayerStateChanged(method, params); + } else if (method.startsWith("Application.On")) { + processApplicationStateChanged(method, params); + } else if (method.startsWith("System.On")) { + processSystemStateChanged(method, params); + } else if (method.startsWith("GUI.OnScreensaver")) { + processScreensaverStateChanged(method, params); + } else { + logger.debug("Received unknown method: {}", method); + } + } + } + + private void processPlayerStateChanged(String method, JsonObject json) { + if ("Player.OnPlay".equals(method)) { + // get the player id and make a new request for the media details + + JsonObject data = json.get("data").getAsJsonObject(); + JsonObject player = data.get("player").getAsJsonObject(); + Integer playerId = player.get("playerid").getAsInt(); + + updateState(KodiState.Play); + + if (data.has("item")) { + JsonObject item = data.get("item").getAsJsonObject(); + String mediaType = item.get("type").getAsString(); + listener.updateMediaType(mediaType); + } + requestPlayerUpdate(playerId, false); + } else if ("Player.OnPause".equals(method)) { + updateState(KodiState.Pause); + } else if ("Player.OnStop".equals(method)) { + // get the end parameter and send an End state if true + JsonObject data = json.get("data").getAsJsonObject(); + Boolean end = data.get("end").getAsBoolean(); + if (end) { + updateState(KodiState.End); + } + updateState(KodiState.Stop); + } else if ("Player.OnSpeedChanged".equals(method)) { + JsonObject data = json.get("data").getAsJsonObject(); + JsonObject player = data.get("player").getAsJsonObject(); + int speed = player.get("speed").getAsInt(); + if (speed == 0) { + updateState(KodiState.Pause); + } else if (speed == 1) { + updateState(KodiState.Play); + } else if (speed < 0) { + updateState(KodiState.Rewind); + } else if (speed > 1) { + updateState(KodiState.FastForward); + } + } else { + logger.warn("Unkown Player Message: {}", method); + } + } + + private void processApplicationStateChanged(String method, JsonObject json) { + if ("Application.OnVolumeChanged".equals(method)) { + // get the player id and make a new request for the media details + JsonObject data = json.get("data").getAsJsonObject(); + + int volume = data.get("volume").getAsInt(); + boolean muted = data.get("muted").getAsBoolean(); + try { + listener.updateVolume(volume); + listener.updateMuted(muted); + } catch (Exception e) { + logger.error("Event listener invoking error", e); + } + + this.volume = volume; + } else { + logger.debug("Unknown event from kodi {}: {}", method, json.toString()); + } + } + + private void processSystemStateChanged(String method, JsonObject json) { + if ("System.OnQuit".equals(method) || "System.OnSleep".equals(method) || "System.OnRestart".equals(method)) { + try { + listener.updateConnectionState(false); + } catch (Exception e) { + logger.error("Event listener invoking error", e); + } + } + } + + private void processScreensaverStateChanged(String method, JsonObject json) { + if ("GUI.OnScreensaverDeactivated".equals(method)) { + updateScreenSaverStatus(false); + } else if ("GUI.OnScreensaverActivated".equals(method)) { + updateScreenSaverStatus(true); + } + } + + private void updateScreenSaverStatus(boolean screenSaverActive) { + try { + listener.updateScreenSaverState(screenSaverActive); + } catch (Exception e) { + logger.error("Event listener invoking error", e); + } + } + + public synchronized void close() { + listener.updateConnectionState(false); + socket = null; + } + + public synchronized void updateVolume() { + if (socket.isConnected()) { + String[] props = { "volume", "version", "name", "muted" }; + + JsonObject params = new JsonObject(); + params.add("properties", getJsonArray(props)); + + JsonElement response = socket.callMethod("Application.GetProperties", params); + + if (response instanceof JsonObject) { + JsonObject result = (JsonObject) response; + if (result.has("volume")) { + volume = result.get("volume").getAsInt(); + listener.updateVolume(volume); + } + if (result.has("muted")) { + boolean muted = result.get("muted").getAsBoolean(); + listener.updateMuted(muted); + } + + } + } else { + listener.updateMuted(false); + listener.updateVolume(100); + } + } + + public synchronized void playURI(String uri) { + JsonObject item = new JsonObject(); + item.addProperty("file", uri); + + JsonObject params = new JsonObject(); + params.add("item", item); + socket.callMethod("Player.Open", params); + } + + public synchronized void showNotification(String message) { + JsonObject params = new JsonObject(); + params.addProperty("title", "openHAB"); + params.addProperty("message", message); + socket.callMethod("GUI.ShowNotification", params); + } + + public boolean checkConnection() { + if (!socket.isConnected()) { + logger.debug("checkConnection: try to connect to kodi {}", wsUri.toString()); + try { + socket.open(); + return socket.isConnected(); + } catch (Throwable t) { + logger.error("exception during connect to {}", wsUri.toString(), t); + try { + socket.close(); + } catch (Exception e) { + } + return false; + } + } else { + return true; + } + } + + public String getConnectionName() { + return wsUri.toString(); + } + + public String getVersion() { + if (socket.isConnected()) { + String[] props = { "version", "name" }; + + JsonObject params = new JsonObject(); + params.add("properties", getJsonArray(props)); + + JsonElement response = socket.callMethod("Application.GetProperties", params); + if (response instanceof JsonObject) { + JsonObject result = (JsonObject) response; + if (result.has("version")) { + JsonObject version = result.get("version").getAsJsonObject(); + int major = version.get("major").getAsInt(); + int minor = version.get("minor").getAsInt(); + String revision = version.get("revision").getAsString(); + return String.format("%d.%d (%s)", major, minor, revision); + } + } + } + return ""; + } + + public void input(String key) { + socket.callMethod("Input." + key); + } + + public void inputText(String text) { + JsonObject params = new JsonObject(); + params.addProperty("text", text); + socket.callMethod("Input.SendText", params); + } + + public void playNotificationSoundURI(String uri) { + playURI(uri); + } + + public void sendSystemCommand(String command) { + String method = "System." + command; + socket.callMethod(method); + } +} \ No newline at end of file diff --git a/addons/binding/pom.xml b/addons/binding/pom.xml index 7c95ef062bb02..0bc1eb9c42280 100644 --- a/addons/binding/pom.xml +++ b/addons/binding/pom.xml @@ -32,6 +32,7 @@ org.openhab.binding.homematic org.openhab.binding.ipp org.openhab.binding.keba + org.openhab.binding.kodi org.openhab.binding.kostalinverter org.openhab.binding.lutron org.openhab.binding.max diff --git a/features/openhab-addons/src/main/feature/feature.xml b/features/openhab-addons/src/main/feature/feature.xml index bfd7bbebefb1f..441ed642a9000 100644 --- a/features/openhab-addons/src/main/feature/feature.xml +++ b/features/openhab-addons/src/main/feature/feature.xml @@ -103,6 +103,11 @@ mvn:org.openhab.binding/org.openhab.binding.keba/${project.version} + + openhab-runtime-base + mvn:org.openhab.binding/org.openhab.binding.kodi/${project.version} + + openhab-runtime-base