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