org.openhab.binding.mpd.internal.MpdBinding.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.mpd.internal.MpdBinding.java

Source

/**
 * Copyright (c) 2010-2015, openHAB.org and others.
 *
 * 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.mpd.internal;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.impl.matchers.GroupMatcher.jobGroupEquals;

import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang.StringUtils;
import org.bff.javampd.MPD;
import org.bff.javampd.MPDAdmin;
import org.bff.javampd.MPDDatabase;
import org.bff.javampd.MPDDatabase.ScopeType;
import org.bff.javampd.MPDFile;
import org.bff.javampd.MPDOutput;
import org.bff.javampd.MPDPlayer;
import org.bff.javampd.MPDPlayer.PlayerStatus;
import org.bff.javampd.MPDPlaylist;
import org.bff.javampd.events.OutputChangeEvent;
import org.bff.javampd.events.OutputChangeListener;
import org.bff.javampd.events.PlayerBasicChangeEvent;
import org.bff.javampd.events.PlayerBasicChangeListener;
import org.bff.javampd.events.PlayerChangeEvent;
import org.bff.javampd.events.TrackPositionChangeEvent;
import org.bff.javampd.events.TrackPositionChangeListener;
import org.bff.javampd.events.VolumeChangeEvent;
import org.bff.javampd.events.VolumeChangeListener;
import org.bff.javampd.exception.MPDConnectionException;
import org.bff.javampd.exception.MPDPlayerException;
import org.bff.javampd.exception.MPDResponseException;
import org.bff.javampd.monitor.MPDStandAloneMonitor;
import org.bff.javampd.objects.MPDSong;
import org.openhab.binding.mpd.MpdBindingProvider;
import org.openhab.binding.mpd.internal.MultiClickDetector.MultiClickListener;
import org.openhab.core.binding.AbstractBinding;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PercentType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.osgi.service.cm.ConfigurationException;
import org.osgi.service.cm.ManagedService;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// to manually detect changes
import org.bff.javampd.events.TrackPositionChangeEvent;
import org.bff.javampd.events.TrackPositionChangeListener;

/**
 * Binding which communicates with (one or many) MPDs. It registers as listener
 * of the MPDs to accomplish real bidirectional communication.
 * 
 * @author Thomas.Eichstaedt-Engelen
 * @author Petr.Klus
 * @author Matthew Bowman
 *
 * @since 0.8.0
 */
public class MpdBinding extends AbstractBinding<MpdBindingProvider> implements ManagedService, VolumeChangeListener,
        PlayerBasicChangeListener, TrackPositionChangeListener, MultiClickListener<Command> {

    private static final String MPD_SCHEDULER_GROUP = "MPD";

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

    private Map<String, MpdPlayerConfig> playerConfigCache = new HashMap<String, MpdPlayerConfig>();

    /** RegEx to validate a mpdPlayer config <code>'^(.*?)\\.(host|port|password)$'</code> */
    private static final Pattern EXTRACT_PLAYER_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.(host|port|password)$");

    /** The value by which the volume is changed by each INCREASE or DECREASE-Event */
    private static final int VOLUME_CHANGE_SIZE = 5;

    /** The connection timeout to wait for a MPD connection */
    private static final int CONNECTION_TIMEOUT = 5000;

    private static MultiClickDetector<Command> clickDetector;

    public MpdBinding() {
        playerConfigCache = new HashMap<String, MpdBinding.MpdPlayerConfig>();
        clickDetector = new MultiClickDetector<Command>(this, 300);
    }

    public void activate() {
        connectAllPlayersAndMonitors();
    }

    public void deactivate() {
        disconnectPlayersAndMonitors();
    }

    /**
     * @{inheritDoc}
     */
    @Override
    public void internalReceiveCommand(String itemName, Command command) {

        MpdBindingProvider provider;
        String matchingPlayerCommand;
        Object params = new Object(); //nothing by default
        if (command instanceof PercentType) {
            //         we have received volume adjustment request
            matchingPlayerCommand = "PERCENT";
            params = command;
        } else {
            matchingPlayerCommand = command.toString();
        }

        provider = findFirstMatchingBindingProvider(itemName, matchingPlayerCommand);

        if (provider == null) {
            logger.warn("cannot find matching binding provider [itemName={}, command={}]", itemName, command);
            return;
        }

        String playerCommand = provider.getPlayerCommand(itemName, matchingPlayerCommand);
        if (StringUtils.isNotBlank(playerCommand)) {
            String playerCommandParam = provider.getPlayerCommandParam(itemName, matchingPlayerCommand);
            if (playerCommandParam != null) {
                params = playerCommandParam;
            }
            executePlayerCommand(playerCommand, params);
        }

    }

    /**
     * Find the first matching {@link MpdBindingProvider} according to 
     * <code>itemName</code> and <code>command</code>. 
     * 
     * @param itemName
     * @param command
     * 
     * @return the matching binding provider or <code>null</code> if no binding
     * provider could be found
     */
    private MpdBindingProvider findFirstMatchingBindingProvider(String itemName, String command) {
        MpdBindingProvider firstMatchingProvider = null;
        for (MpdBindingProvider provider : this.providers) {

            String playerCommand = provider.getPlayerCommand(itemName, command);
            if (playerCommand != null) {
                firstMatchingProvider = provider;
                break;
            }
        }
        return firstMatchingProvider;
    }

    /**
     * Executes the given <code>playerCommandLine</code> on the MPD. The
     * <code>playerCommandLine</code> is split into it's properties 
     * <code>playerId</code> and <code>playerCommand</code>.
     * 
     * @param playerCommandLine the complete commandLine which gets splitted into
     * it's properties.
     */
    private void executePlayerCommand(String playerCommandLine, Object commandParams) {
        String[] commandParts = playerCommandLine.split(":");
        String playerId = commandParts[0];
        String playerCommand = commandParts[1];

        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }

        if (daemon != null) {
            PlayerCommandTypeMapping pCommand = null;
            try {
                pCommand = PlayerCommandTypeMapping.fromString(playerCommand);
                MPDPlayer player = daemon.getMPDPlayer();
                MPDPlaylist playlist = daemon.getMPDPlaylist();
                MPDDatabase db = daemon.getMPDDatabase();

                switch (pCommand) {
                case PAUSE:
                    player.pause();
                    break;
                case PLAY:
                    player.play();
                    break;
                case STOP:
                    player.stop();
                    break;
                case VOLUME_INCREASE:
                    player.setVolume(player.getVolume() + VOLUME_CHANGE_SIZE);
                    break;
                case VOLUME_DECREASE:
                    player.setVolume(player.getVolume() - VOLUME_CHANGE_SIZE);
                    break;
                case NEXT:
                    player.playNext();
                    break;
                case PREV:
                    player.playPrev();
                    break;
                case PLAYSONG:
                    logger.debug("Searching for Song {}", (String) commandParams);
                    Collection<MPDSong> songs = db.find(ScopeType.TITLE, (String) commandParams);

                    Iterator<MPDSong> it = songs.iterator();
                    if (it.hasNext() == true) {
                        MPDSong song = (MPDSong) it.next();

                        logger.debug("Song found: {}", song.getFile());

                        MPDFile file = new MPDFile();
                        file.setPath(song.getFile());

                        playlist.clearPlaylist();
                        playlist.addFileOrDirectory(file);

                    } else {
                        logger.debug("Song not found: {}", (String) commandParams);
                    }

                    break;
                case ENABLE:
                case DISABLE:
                    Integer outputId = Integer.valueOf((String) commandParams);
                    MPDAdmin admin = daemon.getMPDAdmin();
                    MPDOutput output = new MPDOutput(outputId - 1); // internally mpd uses 0-based indexing
                    if (pCommand == PlayerCommandTypeMapping.ENABLE) {
                        admin.enableOutput(output);
                    } else {
                        admin.disableOutput(output);
                    }
                    break;
                case VOLUME:
                    logger.debug("Volume adjustment received: '{}' '{}'", pCommand, commandParams);
                    player.setVolume(((PercentType) commandParams).intValue());
                    break;

                }
            } catch (MPDPlayerException pe) {
                logger.error("error while executing {} command: " + pe.getMessage(), pCommand);
            } catch (Exception e) {
                logger.warn("unknow playerCommand '{}'", playerCommand);
            }
        } else {
            logger.warn("didn't find player configuration instance for playerId '{}'", playerId);
        }

        logger.info("executed commandLine '{}' for player '{}'", playerCommand, playerId);
    }

    private MPD findMPDInstance(String playerId) {
        MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
        if (playerConfig != null) {
            return playerConfig.instance;
        }
        return null;
    }

    private String findPlayerId(Object object) {
        for (String playerId : playerConfigCache.keySet()) {
            MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
            if (playerConfig != null && playerConfig.monitor != null && playerConfig.monitor.equals(object)) {
                return playerId;
            }
        }
        return null;
    }

    /**
     * Implementation of {@link PlayerBasicChangeListener}. Posts the translated
     * type of the {@link PlayerChangeEvent} onto the internal event bus. 
     * <code>PLAYER_STARTED</code>-Events are translated to <code>ON</code> whereas
     * <code>PLAYER_PAUSED</code> and <code>PLAYER_STOPPED</code>-Events are 
     * translated to <code>OFF</code>.
     * 
     * In this case, we use the play state change to trigger full state update, including
     * artist and song name.
     * 
     * @param pbce the event which type is translated and posted onto the internal
     * event bus
     */
    public void playerBasicChange(PlayerBasicChangeEvent pbce) {
        String playerId = findPlayerId(pbce.getSource());

        //   trigger track name update
        determineSongChange(playerId);

        // trigger our play state change      
        determinePlayStateChange(playerId);
    }

    HashMap<String, MPDSong> songInfoCache = new HashMap<String, MPDSong>();
    HashMap<String, PlayerStatus> playerStatusCache = new HashMap<String, PlayerStatus>();

    public void trackPositionChanged(TrackPositionChangeEvent tpce) {

        // cache song name internally, we do not want to fire every time      
        String playerId = findPlayerId(tpce.getSource());
        // update track name            
        determineSongChange(playerId);

        // also update play state      
        determinePlayStateChange(playerId);
    }

    private void broadcastPlayerStateChange(String playerId, PlayerCommandTypeMapping reportTo,
            OnOffType reportStatus) {
        String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, reportTo);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, reportStatus);
            }
        }
    }

    private void determinePlayStateChange(String playerId) {
        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }
        if (daemon != null) {
            try {
                MPDPlayer player = daemon.getMPDPlayer();

                // get the song object here
                PlayerStatus ps = player.getStatus();

                PlayerStatus curPs = playerStatusCache.get(playerId);
                if (curPs != null) {
                    if (ps != curPs) {
                        logger.debug("Play state of '{}' changed", playerId);
                        playerStatusCache.put(playerId, ps);

                        PlayerCommandTypeMapping reportTo;
                        OnOffType reportStatus;
                        // trigger song update
                        if (ps.equals(PlayerStatus.STATUS_PAUSED) || ps.equals(PlayerStatus.STATUS_STOPPED)) {
                            // stopped   
                            reportTo = PlayerCommandTypeMapping.STOP;
                            reportStatus = OnOffType.OFF;
                        } else {
                            //   playing
                            reportTo = PlayerCommandTypeMapping.PLAY;
                            reportStatus = OnOffType.ON;
                        }
                        broadcastPlayerStateChange(playerId, reportTo, reportStatus);
                    } else {
                        // nothing, same state                  
                    }
                } else {
                    playerStatusCache.put(playerId, ps);
                }
            } catch (MPDPlayerException pe) {
                logger.error("error while updating player status: {}" + pe.getMessage(), playerId);
            } catch (Exception e) {
                logger.warn("Failed to communicate with '{}'", playerId);
            }
        } else {
            logger.warn("didn't find player configuration instance for playerId '{}'", playerId);
        }
    }

    private void determineSongChange(String playerId) {
        MPD daemon = findMPDInstance(playerId);
        if (daemon == null) {
            // we give that player another chance -> try to reconnect
            reconnect(playerId);
        }
        if (daemon != null) {
            try {
                MPDPlayer player = daemon.getMPDPlayer();

                // get the song object here
                MPDSong curSong = player.getCurrentSong();

                MPDSong curSongCache = songInfoCache.get(playerId);
                if (curSongCache != null) {
                    // we have some info
                    if (curSong.getId() != curSongCache.getId()) {
                        songInfoCache.put(playerId, curSong);
                        songChanged(playerId, curSong);
                    } else {
                        // nothing, same song
                        // detect play state                  
                    }
                } else {
                    // no info about player's playback state               
                    songInfoCache.put(playerId, curSong);
                    // action the song change&notification 
                    songChanged(playerId, curSong);
                }
            } catch (MPDPlayerException pe) {
                logger.error("error while updating player status: {}" + pe.getMessage(), playerId);
            } catch (Exception e) {
                logger.warn("Failed to communicate with '{}'", playerId);
            }
        } else {
            logger.warn("didn't find player configuration instance for playerId '{}'", playerId);
        }
    }

    private void songChanged(String playerId, MPDSong newSong) {

        logger.debug("Current song {}: {}", playerId, newSong.getTitle().toString());

        String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.TRACKINFO);
        //move to utilities?
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new StringType(newSong.getTitle().toString()));
                logger.debug("Updated title: {} {}", itemName, newSong.getTitle().toString());
            }
        }

        itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.TRACKARTIST);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new StringType(newSong.getArtist().toString()));
                logger.debug("Updated artist: {}, {}", itemName, newSong.getArtist().toString());
            }
        }
    }

    /**
     * More advanced state detection - allows to detect track changes etc. 
     * However, the underlying MPD library does not seem to support this well
     */
    public void playerChanged(PlayerChangeEvent pce) {

    }

    /**
     * Implementation of {@link VolumeChangeListener}. Posts the volume value
     * of the {@link VolumeChangeEvent} onto the internal event bus.
     * 
     * @param vce the event which volume value is posted onto the internal event bus
     */
    public void volumeChanged(VolumeChangeEvent vce) {
        String playerId = findPlayerId(vce.getSource());
        logger.debug("Volume on {} changed to {}", playerId, vce.getVolume());
        String[] itemNames = getItemNamesByPlayerAndPlayerCommand(playerId, PlayerCommandTypeMapping.VOLUME);
        for (String itemName : itemNames) {
            if (StringUtils.isNotBlank(itemName)) {
                eventPublisher.postUpdate(itemName, new PercentType(vce.getVolume()));
            }
        }
    }

    /**
     * Handles MPD output change events. 
     * 
     * @param playerId the playerId which generated the <code>event</code> 
     * @param event the {@link OutputChangeEvent} that occurred
     * 
     * @since 1.6.0
     */
    private void outputChanged(String playerId, OutputChangeEvent event) {
        MPDOutput output = (MPDOutput) event.getSource();
        logger.debug("Output {} changed on player {}, enabled = {}", output.getId(), playerId, output.isEnabled());
        PlayerCommandTypeMapping playerCommand = output.isEnabled() ? PlayerCommandTypeMapping.ENABLE
                : PlayerCommandTypeMapping.DISABLE;
        String[] itemNames = getItemsByPlayerCommandAndOutput(playerId, playerCommand, output);
        for (String itemName : itemNames) {
            eventPublisher.postUpdate(itemName, (State) playerCommand.type);
        }
    }

    private String[] getItemsByPlayerCommandAndOutput(String playerId, PlayerCommandTypeMapping playerCommand,
            MPDOutput output) {
        Set<String> itemNames = new HashSet<String>();
        int outputId = output.getId() + 1; // internally mpd uses 0-based indexes
        for (MpdBindingProvider provider : this.providers) {
            itemNames.addAll(
                    Arrays.asList(provider.getItemNamesByPlayerOutputCommand(playerId, playerCommand, outputId)));
        }
        return itemNames.toArray(new String[itemNames.size()]);
    }

    private String[] getItemNamesByPlayerAndPlayerCommand(String playerId, PlayerCommandTypeMapping playerCommand) {
        Set<String> itemNames = new HashSet<String>();
        for (MpdBindingProvider provider : this.providers) {
            itemNames.addAll(Arrays.asList(provider.getItemNamesByPlayerAndPlayerCommand(playerId, playerCommand)));
        }
        return itemNames.toArray(new String[itemNames.size()]);
    }

    @SuppressWarnings("rawtypes")
    public void updated(Dictionary config) throws ConfigurationException {
        if (config != null) {
            disconnectPlayersAndMonitors();
            cancelScheduler();

            Enumeration keys = config.keys();
            while (keys.hasMoreElements()) {

                String key = (String) keys.nextElement();

                // the config-key enumeration contains additional keys that we
                // don't want to process here ...
                if ("service.pid".equals(key)) {
                    continue;
                }

                Matcher matcher = EXTRACT_PLAYER_CONFIG_PATTERN.matcher(key);
                if (!matcher.matches()) {
                    logger.debug("given mpd player-config-key '" + key
                            + "' does not follow the expected pattern '<playername>.<host|port>'");
                    continue;
                }

                matcher.reset();
                matcher.find();

                String playerId = matcher.group(1);

                MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
                if (playerConfig == null) {
                    playerConfig = new MpdPlayerConfig();
                    playerConfigCache.put(playerId, playerConfig);
                }

                String configKey = matcher.group(2);
                String value = (String) config.get(key);

                if ("host".equals(configKey)) {
                    playerConfig.host = value;
                } else if ("port".equals(configKey)) {
                    playerConfig.port = Integer.valueOf(value);
                } else if ("password".equals(configKey)) {
                    playerConfig.password = value;
                } else {
                    throw new ConfigurationException(configKey,
                            "the given configKey '" + configKey + "' is unknown");
                }

            }

            connectAllPlayersAndMonitors();
            scheduleReconnect();
        }
    }

    private void scheduleReconnect() {
        Scheduler sched;
        try {
            sched = StdSchedulerFactory.getDefaultScheduler();
            JobDetail job = newJob(ReconnectJob.class).withIdentity("Reconnect", MPD_SCHEDULER_GROUP).build();
            CronTrigger trigger = newTrigger().withIdentity("Reconnect", MPD_SCHEDULER_GROUP)
                    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 0 * * ?")).build();

            sched.scheduleJob(job, trigger);
            logger.debug("Scheduled a daily MPD Reconnect of all MPDs");
        } catch (SchedulerException se) {
            logger.warn("scheduling MPD Reconnect failed", se);
        }
    }

    /**
     * Delete all quartz scheduler jobs of the group <code>MPD</code>.
     */
    private void cancelScheduler() {
        try {
            Scheduler sched = StdSchedulerFactory.getDefaultScheduler();
            Set<JobKey> jobKeys = sched.getJobKeys(jobGroupEquals(MPD_SCHEDULER_GROUP));
            if (jobKeys.size() > 0) {
                sched.deleteJobs(new ArrayList<JobKey>(jobKeys));
                logger.debug("Found {} jobs to delete from DefaulScheduler (keys={})", jobKeys.size(), jobKeys);
            }
        } catch (SchedulerException e) {
            logger.warn("Couldn't remove job: {}", e.getMessage());
        }
    }

    /**
     * Connects to all configured {@link MPD}s
     */
    private void connectAllPlayersAndMonitors() {
        logger.debug("MPD binding connecting to players");
        for (String playerId : playerConfigCache.keySet()) {
            connect(playerId);
        }
    }

    /**
     * Connects the player <code>playerId</code> to the given <code>host</code>
     * and <code>port</code> and registers this binding as MPD-Event listener.
     * 
     * @param playerId
     * @param host
     * @param port
     */
    private void connect(final String playerId) {
        MpdPlayerConfig config = null;
        try {

            config = playerConfigCache.get(playerId);
            if (config != null && config.instance == null) {

                MPD mpd = new MPD(config.host, config.port, config.password, CONNECTION_TIMEOUT);

                MPDStandAloneMonitor mpdStandAloneMonitor = new MPDStandAloneMonitor(mpd, 500);
                mpdStandAloneMonitor.addVolumeChangeListener(this);
                mpdStandAloneMonitor.addPlayerChangeListener(this);
                mpdStandAloneMonitor.addTrackPositionChangeListener(this);

                final MpdBinding self = this; // 'this' glue for the inner anon instance
                mpdStandAloneMonitor.addOutputChangeListener(new OutputChangeListener() {

                    @Override
                    public void outputChanged(OutputChangeEvent e) {
                        // We have to 'wrap' the OutputChangeEvent listener
                        // callback and add the playerId so we know which 
                        // player generated the event. There's not enough 
                        // info on just the OutputChangeEvent to derive 
                        // the source player. This 'workaround' is necessary 
                        // to support output control on multiple MPD players.
                        self.outputChanged(playerId, e);

                    }

                });

                Thread monitorThread = new Thread(mpdStandAloneMonitor, "MPD Monitor (player:" + playerId + ")");
                monitorThread.start();

                config.instance = mpd;
                config.monitor = mpdStandAloneMonitor;

                logger.debug("Connected to player '{}' with config {}", playerId, config);
            }

        } catch (MPDConnectionException ce) {
            logger.error("Error connecting to player '" + playerId + "' with config {}", config, ce);
        } catch (UnknownHostException uhe) {
            logger.error("Wrong connection details for player {}", playerId, uhe);
        }
    }

    /**
     * Disconnects all available {@link MPD}s and their {@link MPDStandAloneMonitor}-Thread.
     */
    private void disconnectPlayersAndMonitors() {
        for (String playerId : playerConfigCache.keySet()) {
            disconnect(playerId);
        }
    }

    /**
     * Disconnects the player <code>playerId</code>
     * 
     * @param playerId the id of the player to disconnect from
     */
    private void disconnect(String playerId) {
        try {
            MpdPlayerConfig playerConfig = playerConfigCache.get(playerId);
            MPDStandAloneMonitor monitor = playerConfig.monitor;
            if (monitor != null) {
                monitor.stop();
            }
            MPD mpd = playerConfig.instance;
            if (mpd != null) {
                mpd.close();
            }
        } catch (MPDConnectionException ce) {
            logger.warn("couldn't disconnect player {}", playerId);
        } catch (MPDResponseException re) {
            logger.warn("received response error {}", re.getLocalizedMessage());
        }
    }

    /**
     * Reconnects to <code>playerId</code> that means disconnect first and try
     * to connect again.
     *  
     * @param playerId the id of the player to disconnect from
     */
    private void reconnect(String playerId) {
        logger.info("reconnect player {}", playerId);
        disconnect(playerId);
        connect(playerId);
    }

    /**
     * Reconnects all MPDs and Monitors. Meaning disconnect first and try
     * to connect again.
     */
    private void reconnectAllPlayerAndMonitors() {
        disconnectPlayersAndMonitors();
        connectAllPlayersAndMonitors();
    }

    /**
     * Internal data structure which carries the connection details of one
     * MPD (there could be several)
     *  
     * @author Thomas.Eichstaedt-Engelen
     */
    static class MpdPlayerConfig {

        String host;
        int port;
        String password;

        MPD instance;
        MPDStandAloneMonitor monitor;

        @Override
        public String toString() {
            return "MPD [host=" + host + ", port=" + port + ", password=" + password + "]";
        }

    }

    public void onClick(Command type) {
    }

    public void onDoubleClick(Command type) {
    }

    /**
     * A quartz scheduler job to simply do a reconnection to the MPDs.
     */
    public class ReconnectJob implements Job {
        public void execute(JobExecutionContext context) throws JobExecutionException {
            reconnectAllPlayerAndMonitors();
        }
    }

}