org.openhab.binding.kodi.internal.protocol.KodiConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.kodi.internal.protocol.KodiConnection.java

Source

/**
 * Copyright (c) 2010-2017 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.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.core.cache.ExpiringCacheMap;
import org.eclipse.smarthome.core.library.types.RawType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
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 - Initial contribution
 * @author Christoph Weitkamp - Added channels for opening PVR TV or Radio streams
 * @author Andreas Reinhardt & Christoph Weitkamp - Added channels for thumbnail and fanart
 *
 */
public class KodiConnection implements KodiClientSocketEventListener {

    private final Logger logger = LoggerFactory.getLogger(KodiConnection.class);

    private static final int VOLUMESTEP = 10;
    private static final ExpiringCacheMap<String, RawType> IMAGE_CACHE = new ExpiringCacheMap<>(
            TimeUnit.MINUTES.toMillis(15)); // 15min

    private URI wsUri;
    private URI imageUri;
    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, URI imageUri) {
        this.imageUri = imageUri;
        try {
            close();
            wsUri = new URI("ws", null, hostName, port, "/jsonrpc", null, null);
            socket = new KodiClientSocket(this, wsUri, scheduler);
            checkConnection();
        } catch (URISyntaxException e) {
            logger.error("exception during constructing URI host={}, port={}", hostName, port, e);
        }
    }

    private int getActivePlayer() {
        JsonElement response = socket.callMethod("Player.GetActivePlayers");

        if (response instanceof JsonArray) {
            JsonArray result = response.getAsJsonArray();
            if (result.size() > 0) {
                JsonObject player0 = result.get(0).getAsJsonObject();
                if (player0.has("playerid")) {
                    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", -2);
        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);

        if (response instanceof JsonObject) {
            JsonObject result = response.getAsJsonObject();
            if (result.has("speed")) {
                return result.get("speed").getAsInt();
            }
        }
        return 0;
    }

    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) {
        final String[] properties = { "title", "album", "artist", "director", "thumbnail", "file", "fanart",
                "showtitle", "streamdetails", "channel", "channeltype" };

        JsonObject params = new JsonObject();
        params.addProperty("playerid", activePlayer);
        params.add("properties", getJsonArray(properties));
        JsonElement response = socket.callMethod("Player.GetItem", params);

        if (response instanceof JsonObject) {
            JsonObject result = response.getAsJsonObject();
            if (result.has("item")) {
                JsonObject item = result.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();
                if ("channel".equals(mediaType) && item.has("channeltype")) {
                    String channelType = item.get("channeltype").getAsString();
                    if ("radio".equals(channelType)) {
                        mediaType = "radio";
                    }
                }

                String artist = "";
                if ("movie".equals(mediaType)) {
                    artist = convertFromArray(item.get("director").getAsJsonArray());
                } else {
                    if (item.has("artist")) {
                        artist = convertFromArray(item.get("artist").getAsJsonArray());
                    }
                }

                String channel = "";
                if (item.has("channel")) {
                    channel = item.get("channel").getAsString();
                }

                RawType thumbnail = null;
                if (item.has("thumbnail")) {
                    thumbnail = downloadImage(convertToImageUrl(item.get("thumbnail")));
                }

                RawType fanart = null;
                if (item.has("fanart")) {
                    fanart = downloadImage(convertToImageUrl(item.get("fanart")));
                }

                try {
                    listener.updateAlbum(album);
                    listener.updateTitle(title);
                    listener.updateShowTitle(showTitle);
                    listener.updateArtist(artist);
                    listener.updateMediaType(mediaType);
                    listener.updatePVRChannel(channel);
                    listener.updateThumbnail(thumbnail);
                    listener.updateFanart(fanart);
                } catch (Exception e) {
                    logger.error("Event listener invoking error", e);
                }
            }
        }
    }

    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 String convertToImageUrl(JsonElement element) {
        String text = convertToText(element);
        if (!text.isEmpty()) {
            try {
                // we have to strip ending "/" here because Kodi returns a not valid path and filename
                // "fanart":"image://http%3a%2f%2fthetvdb.com%2fbanners%2ffanart%2foriginal%2f263365-31.jpg/"
                // "thumbnail":"image://http%3a%2f%2fthetvdb.com%2fbanners%2fepisodes%2f263365%2f5640869.jpg/"
                String encodedURL = URLEncoder.encode(StringUtils.stripEnd(text, "/"), "UTF-8");
                return imageUri.resolve(encodedURL).toString();
            } catch (UnsupportedEncodingException e) {
                logger.debug("exception during encoding {}", text, e);
                return null;
            }
        }
        return null;
    }

    private RawType downloadImage(String url) {
        if (StringUtils.isNotEmpty(url)) {
            if (!IMAGE_CACHE.containsKey(url)) {
                IMAGE_CACHE.put(url, () -> {
                    logger.debug("Trying to download the content of URL {}", url);
                    return HttpUtil.downloadImage(url);
                });
            }
            RawType image = IMAGE_CACHE.get(url);
            if (image == null) {
                logger.debug("Failed to download the content of URL {}", url);
                return null;
            } else {
                return image;
            }
        }
        return null;
    }

    public KodiState getState() {
        return currentState;
    }

    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.updateTitle("");
                listener.updateShowTitle("");
                listener.updateArtist("");
                listener.updateMediaType("");
                listener.updatePVRChannel("");
                listener.updateThumbnail(null);
                listener.updateFanart(null);
            }
        } 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);

            requestPlayerUpdate(playerId);
        } 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.OnPropertyChanged".equals(method)) {
            logger.debug("Player.OnPropertyChanged");
        } 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.debug("Unknown event from Kodi {}: {}", method, json.toString());
        }
        listener.updateConnectionState(true);
    }

    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());
        }
        listener.updateConnectionState(true);
    }

    private void processSystemStateChanged(String method, JsonObject json) {
        if ("System.OnQuit".equals(method) || "System.OnRestart".equals(method)
                || "System.OnSleep".equals(method)) {
            listener.updateConnectionState(false);
        } else if ("System.OnWake".equals(method)) {
            listener.updateConnectionState(true);
        } else {
            logger.debug("Unknown event from Kodi {}: {}", method, json.toString());
        }
    }

    private void processScreensaverStateChanged(String method, JsonObject json) {
        if ("GUI.OnScreensaverDeactivated".equals(method)) {
            updateScreenSaverStatus(false);
        } else if ("GUI.OnScreensaverActivated".equals(method)) {
            updateScreenSaverStatus(true);
        } else {
            logger.debug("Unknown event from Kodi {}: {}", method, json.toString());
        }
        listener.updateConnectionState(true);
    }

    private void updateScreenSaverStatus(boolean screenSaverActive) {
        try {
            listener.updateScreenSaverState(screenSaverActive);
        } catch (Exception e) {
            logger.error("Event listener invoking error", e);
        }
    }

    public synchronized void close() {
        if (socket != null && socket.isConnected()) {
            socket.close();
        }
    }

    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 = response.getAsJsonObject();
                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);
    }

    private synchronized JsonArray getChannelGroups(final String channelType) {
        JsonObject params = new JsonObject();
        params.addProperty("channeltype", channelType);
        JsonElement response = socket.callMethod("PVR.GetChannelGroups", params);

        if (response instanceof JsonObject) {
            JsonObject result = response.getAsJsonObject();
            if (result.has("channelgroups")) {
                return result.get("channelgroups").getAsJsonArray();
            }
        }
        return null;
    }

    public int getChannelGroupID(final String channelType, final String channelGroupName) {
        JsonArray channelGroups = getChannelGroups(channelType);
        if (channelGroups instanceof JsonArray) {
            for (JsonElement element : channelGroups) {
                JsonObject channelGroup = (JsonObject) element;
                String label = channelGroup.get("label").getAsString();
                if (StringUtils.equalsIgnoreCase(label, channelGroupName)) {
                    return channelGroup.get("channelgroupid").getAsInt();
                }
            }
        }
        return 0;
    }

    private synchronized JsonArray getChannels(final int channelGroupID) {
        JsonObject params = new JsonObject();
        params.addProperty("channelgroupid", channelGroupID);
        JsonElement response = socket.callMethod("PVR.GetChannels", params);

        if (response instanceof JsonObject) {
            JsonObject result = response.getAsJsonObject();
            if (result.has("channels")) {
                return result.get("channels").getAsJsonArray();
            }
        }
        return null;
    }

    public int getChannelID(final int channelGroupID, final String channelName) {
        JsonArray channels = getChannels(channelGroupID);
        if (channels instanceof JsonArray) {
            for (JsonElement element : channels) {
                JsonObject channel = (JsonObject) element;
                String label = channel.get("label").getAsString();
                if (StringUtils.equalsIgnoreCase(label, channelName)) {
                    return channel.get("channelid").getAsInt();
                }
            }
        }
        return 0;
    }

    public synchronized void playPVRChannel(final int channelID) {
        JsonObject item = new JsonObject();
        item.addProperty("channelid", channelID);

        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);
            try {
                socket.open();
                return socket.isConnected();
            } catch (Exception e) {
                logger.error("exception during connect to {}", wsUri, e);
                socket.close();
                return false;
            }
        } else {
            // Ping Kodi with the get version command. This prevents the idle
            // timeout on the websocket.
            return !getVersion().isEmpty();
        }
    }

    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 = response.getAsJsonObject();
                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);
    }

}