org.eclipse.smarthome.binding.sonos.handler.ZonePlayerHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.smarthome.binding.sonos.handler.ZonePlayerHandler.java

Source

/**
 * Copyright (c) 2014-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.eclipse.smarthome.binding.sonos.handler;

import static org.eclipse.smarthome.binding.sonos.SonosBindingConstants.*;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.sonos.SonosBindingConstants;
import org.eclipse.smarthome.binding.sonos.config.ZonePlayerConfiguration;
import org.eclipse.smarthome.binding.sonos.internal.SonosAlarm;
import org.eclipse.smarthome.binding.sonos.internal.SonosEntry;
import org.eclipse.smarthome.binding.sonos.internal.SonosMetaData;
import org.eclipse.smarthome.binding.sonos.internal.SonosXMLParser;
import org.eclipse.smarthome.binding.sonos.internal.SonosZoneGroup;
import org.eclipse.smarthome.binding.sonos.internal.SonosZonePlayerState;
import org.eclipse.smarthome.config.discovery.DiscoveryServiceRegistry;
import org.eclipse.smarthome.core.library.types.DecimalType;
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.OpenClosedType;
import org.eclipse.smarthome.core.library.types.PercentType;
import org.eclipse.smarthome.core.library.types.PlayPauseType;
import org.eclipse.smarthome.core.library.types.RawType;
import org.eclipse.smarthome.core.library.types.RewindFastforwardType;
import org.eclipse.smarthome.core.library.types.StringType;
import org.eclipse.smarthome.core.library.types.UpDownType;
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.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.thing.binding.ThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
import org.eclipse.smarthome.io.transport.upnp.UpnpIOParticipant;
import org.eclipse.smarthome.io.transport.upnp.UpnpIOService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Lists;

/**
 * The {@link ZonePlayerHandler} is responsible for handling commands, which are
 * sent to one of the channels.
 *
 * @author Karel Goderis - Initial contribution
 *
 */
public class ZonePlayerHandler extends BaseThingHandler implements UpnpIOParticipant {

    private Logger logger = LoggerFactory.getLogger(ZonePlayerHandler.class);

    private final static String ANALOG_LINE_IN_URI = "x-rincon-stream:";
    private final static String OPTICAL_LINE_IN_URI = "x-sonos-htastream:";
    private final static String QUEUE_URI = "x-rincon-queue:";
    private final static String GROUP_URI = "x-rincon:";
    private final static String STREAM_URI = "x-sonosapi-stream:";
    private final static String FILE_URI = "x-file-cifs:";
    private final static String SPDIF = ":spdif";

    private UpnpIOService service;
    private DiscoveryServiceRegistry discoveryServiceRegistry;
    private ScheduledFuture<?> pollingJob;
    private SonosZonePlayerState savedState = null;

    private final static Collection<String> SERVICE_SUBSCRIPTIONS = Lists.newArrayList("DeviceProperties",
            "AVTransport", "ZoneGroupTopology", "GroupManagement", "RenderingControl", "AudioIn", "HTControl");
    private Map<String, Boolean> subscriptionState = new HashMap<String, Boolean>();
    protected final static int SUBSCRIPTION_DURATION = 1800;
    private static final int SOCKET_TIMEOUT = 5000;

    /**
     * Default notification timeout
     */
    private static final int NOTIFICATION_TIMEOUT = 20000;

    /**
     * Intrinsic lock used to synchronize the execution of notification sounds
     */
    private final Object notificationLock = new Object();

    /**
     * Separate sound volume used for the notification
     */
    private String notificationSoundVolume = null;

    /**
     * {@link ThingHandler} instance of the coordinator speaker used for control delegation
     */
    private ZonePlayerHandler coordinatorHandler;

    /**
     * The default refresh interval when not specified in channel configuration.
     */
    private static final int DEFAULT_REFRESH_INTERVAL = 60;

    private Map<String, String> stateMap = Collections.synchronizedMap(new HashMap<String, String>());

    private final Object upnpLock = new Object();

    private final Object stateLock = new Object();

    private Runnable pollingRunnable = new Runnable() {

        @Override
        public void run() {
            try {
                logger.debug("Polling job");

                // First check if the Sonos zone is set in the UPnP service registry
                // If not, set the thing state to OFFLINE and wait for the next poll
                if (!isUpnpDeviceRegistered()) {
                    logger.debug("UPnP device {} not yet registered", getUDN());
                    updateStatus(ThingStatus.OFFLINE);
                    synchronized (upnpLock) {
                        subscriptionState = new HashMap<String, Boolean>();
                    }
                    return;
                }

                // Check if the Sonos zone can be joined
                // If not, set the thing state to OFFLINE and do nothing else
                updatePlayerState();
                if (getThing().getStatus() != ThingStatus.ONLINE) {
                    return;
                }

                addSubscription();

                updateZoneInfo();
                updateRunningAlarmProperties();
                updateLed();
                updateSleepTimerDuration();
            } catch (Exception e) {
                logger.debug("Exception during poll : {}", e);
            }
        }
    };

    private String opmlUrl;

    public ZonePlayerHandler(Thing thing, UpnpIOService upnpIOService,
            DiscoveryServiceRegistry discoveryServiceRegistry, String opmlUrl) {
        super(thing);
        this.opmlUrl = opmlUrl;
        logger.debug("Creating a ZonePlayerHandler for thing '{}'", getThing().getUID());
        if (upnpIOService != null) {
            this.service = upnpIOService;
        }
        if (discoveryServiceRegistry != null) {
            this.discoveryServiceRegistry = discoveryServiceRegistry;
        }

    }

    @Override
    public void dispose() {
        logger.debug("Handler disposed for thing {}", getThing().getUID());

        if (pollingJob != null && !pollingJob.isCancelled()) {
            pollingJob.cancel(true);
            pollingJob = null;
        }

        removeSubscription();
    }

    @Override
    public void initialize() {
        logger.debug("initializing handler for thing {}", getThing().getUID());

        if (migrateThingType()) {
            // we change the type, so we might need a different handler -> let's finish
            return;
        }

        if (getUDN() != null) {
            updateStatus(ThingStatus.ONLINE);
            onUpdate();
            super.initialize();
        } else {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
            logger.warn("Cannot initalize the zoneplayer. UDN not set.");
        }
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        if (command instanceof RefreshType) {
            updateChannel(channelUID.getId());
        } else {
            switch (channelUID.getId()) {
            case LED:
                setLed(command);
                break;
            case MUTE:
                setMute(command);
                break;
            case NOTIFICATIONSOUND:
                scheduleNotificationSound(command);
                break;
            case NOTIFICATIONVOLUME:
                setNotificationSoundVolume(command);
                break;
            case STOP:
                try {
                    getCoordinatorHandler().stop();
                } catch (IllegalStateException e) {
                    logger.warn("Cannot handle stop command ({})", e.getMessage());
                }
                break;
            case VOLUME:
                setVolumeForGroup(command);
                break;
            case ADD:
                addMember(command);
                break;
            case REMOVE:
                removeMember(command);
                break;
            case STANDALONE:
                becomeStandAlonePlayer();
                break;
            case PUBLICADDRESS:
                publicAddress();
                break;
            case RADIO:
                playRadio(command);
                break;
            case FAVORITE:
                playFavorite(command);
                break;
            case ALARM:
                setAlarm(command);
                break;
            case SNOOZE:
                snoozeAlarm(command);
                break;
            case SAVEALL:
                saveAllPlayerState();
                break;
            case RESTOREALL:
                restoreAllPlayerState();
                break;
            case SAVE:
                saveState();
                break;
            case RESTORE:
                restoreState();
                break;
            case PLAYLIST:
                playPlayList(command);
                break;
            case PLAYQUEUE:
                playQueue();
                break;
            case PLAYTRACK:
                playTrack(command);
                break;
            case PLAYURI:
                playURI(command);
                break;
            case PLAYLINEIN:
                playLineIn(command);
                break;
            case CONTROL:
                try {
                    if (command instanceof PlayPauseType) {
                        if (command == PlayPauseType.PLAY) {
                            getCoordinatorHandler().play();
                        } else if (command == PlayPauseType.PAUSE) {
                            getCoordinatorHandler().pause();
                        }
                    }
                    if (command instanceof NextPreviousType) {
                        if (command == NextPreviousType.NEXT) {
                            getCoordinatorHandler().next();
                        } else if (command == NextPreviousType.PREVIOUS) {
                            getCoordinatorHandler().previous();
                        }
                    }
                    if (command instanceof RewindFastforwardType) {
                        // Rewind and Fast Forward are currently not implemented by the binding
                    }
                } catch (IllegalStateException e) {
                    logger.warn("Cannot handle control command ({})", e.getMessage());
                }
                break;
            case SLEEPTIMER:
                setSleepTimer(command);
                break;
            case SHUFFLE:
                setShuffle(command);
                break;
            case REPEAT:
                setRepeat(command);
                break;
            default:
                break;
            }
        }
    }

    private void restoreAllPlayerState() {
        Collection<Thing> allThings = thingRegistry.getAll();
        for (Thing aThing : allThings) {
            if (SonosBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(aThing.getThingTypeUID())) {
                ZonePlayerHandler handler = (ZonePlayerHandler) aThing.getHandler();
                handler.restoreState();
            }
        }
    }

    private void saveAllPlayerState() {
        Collection<Thing> allThings = thingRegistry.getAll();
        for (Thing aThing : allThings) {
            if (SonosBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(aThing.getThingTypeUID())) {
                ZonePlayerHandler handler = (ZonePlayerHandler) aThing.getHandler();
                handler.saveState();
            }
        }
    }

    @Override
    public void onValueReceived(String variable, String value, String service) {

        if (getThing().getStatus() == ThingStatus.ONLINE) {

            logger.trace("Received pair '{}':'{}' (service '{}') for thing '{}'",
                    new Object[] { variable, value, service, this.getThing().getUID() });

            String oldValue = this.stateMap.get(variable);
            if (shouldIgnoreVariableUpdate(variable, value, oldValue)) {
                return;
            }

            this.stateMap.put(variable, value);

            // pre-process some variables, eg XML processing
            if (service.equals("AVTransport") && variable.equals("LastChange")) {
                Map<String, String> parsedValues = SonosXMLParser.getAVTransportFromXML(value);
                for (String parsedValue : parsedValues.keySet()) {
                    // Update the transport state after the update of the media information
                    // to not break the notification mechanism
                    if (!parsedValue.equals("TransportState")) {
                        onValueReceived(parsedValue, parsedValues.get(parsedValue), "AVTransport");
                    }
                    // Translate AVTransportURI/AVTransportURIMetaData to CurrentURI/CurrentURIMetaData
                    // for a compatibility with the result of the action GetMediaInfo
                    if (parsedValue.equals("AVTransportURI")) {
                        onValueReceived("CurrentURI", parsedValues.get(parsedValue), service);
                    } else if (parsedValue.equals("AVTransportURIMetaData")) {
                        onValueReceived("CurrentURIMetaData", parsedValues.get(parsedValue), service);
                    }
                }
                updateMediaInformation();
                if (parsedValues.get("TransportState") != null) {
                    onValueReceived("TransportState", parsedValues.get("TransportState"), "AVTransport");
                }
            }

            if (service.equals("RenderingControl") && variable.equals("LastChange")) {
                Map<String, String> parsedValues = SonosXMLParser.getRenderingControlFromXML(value);
                for (String parsedValue : parsedValues.keySet()) {
                    onValueReceived(parsedValue, parsedValues.get(parsedValue), "RenderingControl");
                }
            }

            // update the appropriate channel
            switch (variable) {
            case "TransportState":
                updateChannel(STATE);
                updateChannel(CONTROL);
                dispatchOnAllGroupMembers(variable, value, service);
                break;
            case "CurrentPlayMode":
                updateChannel(SHUFFLE);
                updateChannel(REPEAT);
                dispatchOnAllGroupMembers(variable, value, service);
                break;
            case "CurrentLEDState":
                updateChannel(LED);
                break;
            case "ZoneName":
                updateState(ZONENAME, (stateMap.get("ZoneName") != null) ? new StringType(stateMap.get("ZoneName"))
                        : UnDefType.UNDEF);
                break;
            case "CurrentZoneName":
                updateChannel(ZONENAME);
                break;
            case "ZoneGroupState":
                updateChannel(ZONEGROUP);
                updateChannel(COORDINATOR);
                // Update coordinator after a change is made to the grouping of Sonos players
                updateGroupCoordinator();
                updateMediaInformation();
                // Update state and control channels for the group members with the coordinator values
                if (stateMap.get("TransportState") != null) {
                    dispatchOnAllGroupMembers("TransportState", stateMap.get("TransportState"), "AVTransport");
                }
                // Update shuffle and repeat channels for the group members with the coordinator values
                if (stateMap.get("CurrentPlayMode") != null) {
                    dispatchOnAllGroupMembers("CurrentPlayMode", stateMap.get("CurrentPlayMode"), "AVTransport");
                }
                break;
            case "LocalGroupUUID":
                updateChannel(ZONEGROUPID);
                break;
            case "GroupCoordinatorIsLocal":
                updateChannel(LOCALCOORDINATOR);
                break;
            case "VolumeMaster":
                updateChannel(VOLUME);
                break;
            case "MuteMaster":
                updateChannel(MUTE);
                break;
            case "LineInConnected":
            case "TOSLinkConnected":
                updateChannel(LINEIN);
                break;
            case "AlarmRunning":
                updateChannel(ALARMRUNNING);
                break;
            case "RunningAlarmProperties":
                updateChannel(ALARMPROPERTIES);
                break;
            case "CurrentURIFormatted":
                updateChannel(CURRENTTRACK);
                break;
            case "CurrentTitle":
                updateChannel(CURRENTTITLE);
                break;
            case "CurrentArtist":
                updateChannel(CURRENTARTIST);
                break;
            case "CurrentAlbum":
                updateChannel(CURRENTALBUM);
                break;
            case "CurrentURI":
                updateChannel(CURRENTTRANSPORTURI);
                break;
            case "CurrentTrackURI":
                updateChannel(CURRENTTRACKURI);
                break;
            case "CurrentAlbumArtURI":
                updateChannel(CURRENTALBUMART);
                updateChannel(CURRENTALBUMARTURL);
                break;

            case "CurrentSleepTimerGeneration":
                if (value.equals("0")) {
                    updateState(SLEEPTIMER, new DecimalType(0));
                }
                break;
            case "SleepTimerGeneration":
                if (value.equals("0")) {
                    updateState(SLEEPTIMER, new DecimalType(0));
                } else {
                    updateSleepTimerDuration();
                }
                break;
            case "RemainingSleepTimerDuration":
                updateState(SLEEPTIMER,
                        (stateMap.get("RemainingSleepTimerDuration") != null)
                                ? new DecimalType(
                                        sleepStrTimeToSeconds(stateMap.get("RemainingSleepTimerDuration")))
                                : UnDefType.UNDEF);
                break;

            default:
                break;
            }
        }

    }

    private void dispatchOnAllGroupMembers(String variable, String value, String service) {
        if (isCoordinator()) {
            for (String member : getOtherZoneGroupMembers()) {
                try {
                    ZonePlayerHandler memberHandler = getHandlerByName(member);
                    if (memberHandler != null && memberHandler.getThing() != null
                            && ThingStatus.ONLINE.equals(memberHandler.getThing().getStatus())) {
                        memberHandler.onValueReceived(variable, value, service);
                    }
                } catch (IllegalStateException e) {
                    logger.warn("Cannot update channel for group member ({})", e.getMessage());
                }
            }
        }
    }

    private URL getAlbumArtUrl() {
        URL url = null;
        String albumArtURI = stateMap.get("CurrentAlbumArtURI");
        if (albumArtURI != null) {
            try {
                if (albumArtURI.startsWith("http")) {
                    url = new URL(albumArtURI);
                } else if (albumArtURI.startsWith("/")) {
                    URL serviceDescrUrl = service.getDescriptorURL(this);
                    if (serviceDescrUrl != null) {
                        url = new URL(serviceDescrUrl.getProtocol(), serviceDescrUrl.getHost(),
                                serviceDescrUrl.getPort(), albumArtURI);
                    }
                }
            } catch (MalformedURLException e) {
                logger.debug("Failed to build a valid album art URL from {}: {}", albumArtURI, e.getMessage());
                url = null;
            }
        }
        return url;
    }

    private String getContentTypeFromUrl(URL url) {
        if (url == null) {
            return null;
        }
        String contentType;
        InputStream input = null;
        try {
            URLConnection connection = url.openConnection();
            contentType = connection.getContentType();
            logger.debug("Content type from headers: {}", contentType);
            if (contentType == null) {
                input = connection.getInputStream();
                contentType = URLConnection.guessContentTypeFromStream(input);
                logger.debug("Content type from data: {}", contentType);
                if (contentType == null) {
                    contentType = RawType.DEFAULT_MIME_TYPE;
                }
            }
        } catch (IOException e) {
            logger.debug("Failed to identify content type from URL: {}", e.getMessage());
            contentType = RawType.DEFAULT_MIME_TYPE;
        } finally {
            IOUtils.closeQuietly(input);
        }
        return contentType;
    }

    protected void updateChannel(String channeldD) {
        if (!isLinked(channeldD)) {
            return;
        }

        URL url;

        State newState = UnDefType.UNDEF;
        switch (channeldD) {
        case STATE:
            if (stateMap.get("TransportState") != null) {
                newState = new StringType(stateMap.get("TransportState"));
            }
            break;
        case CONTROL:
            if (stateMap.get("TransportState") != null) {
                if (stateMap.get("TransportState").equals("PLAYING")) {
                    newState = PlayPauseType.PLAY;
                } else if (stateMap.get("TransportState").equals("STOPPED")) {
                    newState = PlayPauseType.PAUSE;
                } else if (stateMap.get("TransportState").equals("PAUSED_PLAYBACK")) {
                    newState = PlayPauseType.PAUSE;
                }
            }
            break;
        case SHUFFLE:
            if (stateMap.get("CurrentPlayMode") != null) {
                newState = isShuffleActive() ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case REPEAT:
            if (stateMap.get("CurrentPlayMode") != null) {
                newState = new StringType(getRepeatMode());
            }
            break;
        case LED:
            if (stateMap.get("CurrentLEDState") != null) {
                newState = stateMap.get("CurrentLEDState").equals("On") ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case ZONENAME:
            if (stateMap.get("CurrentZoneName") != null) {
                newState = new StringType(stateMap.get("CurrentZoneName"));
            }
            break;
        case ZONEGROUP:
            if (stateMap.get("ZoneGroupState") != null) {
                newState = new StringType(stateMap.get("ZoneGroupState"));
            }
            break;
        case ZONEGROUPID:
            if (stateMap.get("LocalGroupUUID") != null) {
                newState = new StringType(stateMap.get("LocalGroupUUID"));
            }
            break;
        case COORDINATOR:
            newState = new StringType(getCoordinator());
            break;
        case LOCALCOORDINATOR:
            if (stateMap.get("GroupCoordinatorIsLocal") != null) {
                newState = stateMap.get("GroupCoordinatorIsLocal").equals("true") ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case VOLUME:
            if (stateMap.get("VolumeMaster") != null) {
                newState = new PercentType(stateMap.get("VolumeMaster"));
            }
            break;
        case MUTE:
            if (stateMap.get("MuteMaster") != null) {
                newState = stateMap.get("MuteMaster").equals("1") ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case LINEIN:
            if (stateMap.get("LineInConnected") != null) {
                newState = stateMap.get("LineInConnected").equals("true") ? OnOffType.ON : OnOffType.OFF;
            } else if (stateMap.get("TOSLinkConnected") != null) {
                newState = stateMap.get("TOSLinkConnected").equals("true") ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case ALARMRUNNING:
            if (stateMap.get("AlarmRunning") != null) {
                newState = stateMap.get("AlarmRunning").equals("1") ? OnOffType.ON : OnOffType.OFF;
            }
            break;
        case ALARMPROPERTIES:
            if (stateMap.get("RunningAlarmProperties") != null) {
                newState = new StringType(stateMap.get("RunningAlarmProperties"));
            }
            break;
        case CURRENTTRACK:
            if (stateMap.get("CurrentURIFormatted") != null) {
                newState = new StringType(stateMap.get("CurrentURIFormatted"));
            }
            break;
        case CURRENTTITLE:
            if (stateMap.get("CurrentTitle") != null) {
                newState = new StringType(stateMap.get("CurrentTitle"));
            }
            break;
        case CURRENTARTIST:
            if (stateMap.get("CurrentArtist") != null) {
                newState = new StringType(stateMap.get("CurrentArtist"));
            }
            break;
        case CURRENTALBUM:
            if (stateMap.get("CurrentAlbum") != null) {
                newState = new StringType(stateMap.get("CurrentAlbum"));
            }
            break;
        case CURRENTALBUMART:
            url = getAlbumArtUrl();
            if (url != null) {
                String contentType = getContentTypeFromUrl(url);
                InputStream input = null;
                try {
                    input = url.openStream();
                    newState = new RawType(IOUtils.toByteArray(input), contentType);
                } catch (IOException e) {
                    logger.debug("Failed to download the album cover art: {}", e.getMessage());
                    newState = UnDefType.UNDEF;
                } finally {
                    IOUtils.closeQuietly(input);
                }
            }
            break;
        case CURRENTALBUMARTURL:
            url = getAlbumArtUrl();
            if (url != null) {
                newState = new StringType(url.toExternalForm());
            }
            break;
        case CURRENTTRANSPORTURI:
            if (stateMap.get("CurrentURI") != null) {
                newState = new StringType(stateMap.get("CurrentURI"));
            }
            break;
        case CURRENTTRACKURI:
            if (stateMap.get("CurrentTrackURI") != null) {
                newState = new StringType(stateMap.get("CurrentTrackURI"));
            }
            break;
        default:
            newState = null;
            break;
        }
        if (newState != null) {
            updateState(channeldD, newState);
        }
    }

    /**
     * CurrentURI will not change, but will trigger change of CurrentURIFormated
     * CurrentTrackMetaData will not change, but will trigger change of Title, Artist, Album
     */
    private boolean shouldIgnoreVariableUpdate(String variable, String value, String oldValue) {
        return !hasValueChanged(value, oldValue) && !isQueueEvent(variable);
    }

    private boolean hasValueChanged(String value, String oldValue) {
        return oldValue != null ? !oldValue.equals(value) : value != null;
    }

    /**
     * Similar to the AVTransport eventing, the Queue events its state variables
     * as sub values within a synthesized LastChange state variable.
     */
    private boolean isQueueEvent(String variable) {
        return "LastChange".equals(variable);
    }

    private void updateGroupCoordinator() {
        try {
            coordinatorHandler = getHandlerByName(getCoordinator());
        } catch (IllegalStateException e) {
            logger.warn("Cannot update the group coordinator ({})", e.getMessage());
            coordinatorHandler = null;
        }
    }

    private boolean isUpnpDeviceRegistered() {
        return service.isRegistered(this);
    }

    private void addSubscription() {
        synchronized (upnpLock) {
            // Set up GENA Subscriptions
            if (service.isRegistered(this)) {
                for (String subscription : SERVICE_SUBSCRIPTIONS) {
                    if ((subscriptionState.get(subscription) == null)
                            || !subscriptionState.get(subscription).booleanValue()) {
                        logger.debug("{}: Subscribing to service {}...", getUDN(), subscription);
                        service.addSubscription(this, subscription, SUBSCRIPTION_DURATION);
                        subscriptionState.put(subscription, true);
                    }
                }
            }
        }
    }

    private void removeSubscription() {
        synchronized (upnpLock) {
            // Set up GENA Subscriptions
            if (service.isRegistered(this)) {
                for (String subscription : SERVICE_SUBSCRIPTIONS) {
                    if ((subscriptionState.get(subscription) != null)
                            && subscriptionState.get(subscription).booleanValue()) {
                        logger.debug("{}: Unsubscribing from service {}...", getUDN(), subscription);
                        service.removeSubscription(this, subscription);
                    }
                }
            }
            subscriptionState = new HashMap<String, Boolean>();
            service.unregisterParticipant(this);
        }
    }

    @Override
    public void onServiceSubscribed(String service, boolean succeeded) {
        synchronized (upnpLock) {
            logger.debug("{}: Subscription to service {} {}", getUDN(), service,
                    succeeded ? "succeeded" : "failed");
            subscriptionState.put(service, succeeded);
        }
    }

    private void onUpdate() {
        if (pollingJob == null || pollingJob.isCancelled()) {
            ZonePlayerConfiguration config = getConfigAs(ZonePlayerConfiguration.class);
            // use default if not specified
            int refreshInterval = DEFAULT_REFRESH_INTERVAL;
            if (config.refresh != null) {
                refreshInterval = config.refresh.intValue();
            }
            pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, refreshInterval, TimeUnit.SECONDS);
        }
    }

    private void updatePlayerState() {
        Map<String, String> result = service.invokeAction(this, "DeviceProperties", "GetZoneInfo", null);
        if (result.isEmpty()) {
            if (!ThingStatus.OFFLINE.equals(getThing().getStatus())) {
                logger.debug("Sonos player {} is not available in local network", getUDN());
                updateStatus(ThingStatus.OFFLINE);
                synchronized (upnpLock) {
                    subscriptionState = new HashMap<String, Boolean>();
                }
            }
        } else if (!ThingStatus.ONLINE.equals(getThing().getStatus())) {
            logger.debug("Sonos player {} has been found in local network", getUDN());
            updateStatus(ThingStatus.ONLINE);
        }
    }

    protected void updateMediaInfo() {
        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("InstanceID", "0");

        Map<String, String> result = service.invokeAction(this, "AVTransport", "GetMediaInfo", inputs);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
        updateMediaInformation();
    }

    protected void updateCurrentZoneName() {
        Map<String, String> result = service.invokeAction(this, "DeviceProperties", "GetZoneAttributes", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "DeviceProperties");
        }
    }

    protected void updateLed() {
        Map<String, String> result = service.invokeAction(this, "DeviceProperties", "GetLEDState", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "DeviceProperties");
        }
    }

    protected void updateTime() {
        Map<String, String> result = service.invokeAction(this, "AlarmClock", "GetTimeNow", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AlarmClock");
        }
    }

    protected void updatePosition() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "GetPositionInfo", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    protected void updateRunningAlarmProperties() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "GetRunningAlarmProperties", null);

        String alarmID = result.get("AlarmID");
        String loggedStartTime = result.get("LoggedStartTime");
        String newStringValue = null;
        if (alarmID != null && loggedStartTime != null) {
            newStringValue = alarmID + " - " + loggedStartTime;
        } else {
            newStringValue = "No running alarm";
        }
        result.put("RunningAlarmProperties", newStringValue);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    protected void updateZoneInfo() {
        Map<String, String> result = service.invokeAction(this, "DeviceProperties", "GetZoneInfo", null);
        Map<String, String> result2 = service.invokeAction(this, "DeviceProperties", "GetZoneAttributes", null);

        result.putAll(result2);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "DeviceProperties");
        }
    }

    public String getCoordinator() {

        if (stateMap.get("ZoneGroupState") != null) {

            Collection<SonosZoneGroup> zoneGroups = SonosXMLParser
                    .getZoneGroupFromXML(stateMap.get("ZoneGroupState"));

            for (SonosZoneGroup zg : zoneGroups) {
                if (zg.getMembers().contains(getUDN())) {
                    return zg.getCoordinator();
                }
            }
        }

        return getUDN();
    }

    public boolean isCoordinator() {
        return getUDN().equals(getCoordinator());
    }

    protected void updateMediaInformation() {

        String currentURI = getCurrentURI();
        SonosMetaData currentTrack = getTrackMetadata();
        SonosMetaData currentUriMetaData = getCurrentURIMetadata();

        String artist = null;
        String album = null;
        String title = null;
        String resultString = null;
        boolean needsUpdating = false;

        if (currentURI == null) {
            // Do nothing
        }

        else if (currentURI.isEmpty()) {
            // Reset data
            needsUpdating = true;
        }

        else if (currentURI.contains(GROUP_URI)) {
            // The Sonos is a slave member of a group, we do nothing
            // The media information will be updated by the coordinator
            // Notification of group change occurs later, so we just check the URI
        }

        else if (isPlayingStream(currentURI)) {
            // Radio stream (tune-in)
            boolean opmlUrlSucceeded = false;
            if (opmlUrl != null) {
                String stationID = StringUtils.substringBetween(currentURI, ":s", "?sid");
                String mac = getMACAddress();
                if (stationID != null && !stationID.isEmpty() && mac != null && !mac.isEmpty()) {
                    String url = opmlUrl;
                    url = StringUtils.replace(url, "%id", stationID);
                    url = StringUtils.replace(url, "%serial", mac);

                    String response = null;
                    try {
                        response = HttpUtil.executeUrl("GET", url, SOCKET_TIMEOUT);
                    } catch (IOException e) {
                        logger.debug("Request to device failed: {}", e);
                    }

                    if (response != null) {
                        List<String> fields = SonosXMLParser.getRadioTimeFromXML(response);

                        if (fields != null && fields.size() > 0) {

                            opmlUrlSucceeded = true;

                            resultString = new String();
                            // radio name should be first field
                            title = fields.get(0);

                            Iterator<String> listIterator = fields.listIterator();
                            while (listIterator.hasNext()) {
                                String field = listIterator.next();
                                resultString = resultString + field;
                                if (listIterator.hasNext()) {
                                    resultString = resultString + " - ";
                                }
                            }

                            needsUpdating = true;
                        }
                    }
                }
            }
            if (!opmlUrlSucceeded) {
                if (currentUriMetaData != null) {
                    title = currentUriMetaData.getTitle();
                    if ((currentTrack == null) || (currentTrack.getStreamContent() == null)
                            || currentTrack.getStreamContent().isEmpty()) {
                        resultString = title;
                    } else {
                        resultString = title + " - " + currentTrack.getStreamContent();
                    }
                    needsUpdating = true;
                }
            }
        }

        else if (isPlayingLineIn(currentURI)) {
            if (currentTrack != null) {
                title = currentTrack.getTitle();
                resultString = title;
                needsUpdating = true;
            }
        }

        else if (!currentURI.contains("x-rincon-mp3") && !currentURI.contains("x-sonosapi")) {
            if (currentTrack != null) {
                artist = !currentTrack.getAlbumArtist().isEmpty() ? currentTrack.getAlbumArtist()
                        : currentTrack.getCreator();
                album = currentTrack.getAlbum();
                title = currentTrack.getTitle();
                resultString = artist + " - " + album + " - " + title;
                needsUpdating = true;
            }
        }

        if (needsUpdating) {
            String albumArtURI = (currentTrack != null && currentTrack.getAlbumArtUri() != null
                    && !currentTrack.getAlbumArtUri().isEmpty()) ? currentTrack.getAlbumArtUri() : "";
            for (String member : getZoneGroupMembers()) {
                try {
                    ZonePlayerHandler memberHandler = getHandlerByName(member);
                    if (memberHandler != null && memberHandler.getThing() != null
                            && ThingStatus.ONLINE.equals(memberHandler.getThing().getStatus())) {
                        memberHandler.onValueReceived("CurrentArtist", (artist != null) ? artist : "",
                                "AVTransport");
                        memberHandler.onValueReceived("CurrentAlbum", (album != null) ? album : "", "AVTransport");
                        memberHandler.onValueReceived("CurrentTitle", (title != null) ? title : "", "AVTransport");
                        memberHandler.onValueReceived("CurrentURIFormatted",
                                (resultString != null) ? resultString : "", "AVTransport");
                        memberHandler.onValueReceived("CurrentAlbumArtURI", albumArtURI, "AVTransport");
                    }
                } catch (IllegalStateException e) {
                    logger.warn("Cannot update media data for group member ({})", e.getMessage());
                }
            }
        }
    }

    public boolean isGroupCoordinator() {
        String value = stateMap.get("GroupCoordinatorIsLocal");
        if (value != null) {
            return value.equals("true") ? true : false;
        }

        return false;

    }

    @Override
    public String getUDN() {
        return getConfigAs(ZonePlayerConfiguration.class).udn;
    }

    public String getCurrentURI() {
        return stateMap.get("CurrentURI");
    }

    public SonosMetaData getCurrentURIMetadata() {
        if (stateMap.get("CurrentURIMetaData") != null && !stateMap.get("CurrentURIMetaData").isEmpty()) {
            return SonosXMLParser.getMetaDataFromXML(stateMap.get("CurrentURIMetaData"));
        } else {
            return null;
        }
    }

    public SonosMetaData getTrackMetadata() {
        if (stateMap.get("CurrentTrackMetaData") != null && !stateMap.get("CurrentTrackMetaData").isEmpty()) {
            return SonosXMLParser.getMetaDataFromXML(stateMap.get("CurrentTrackMetaData"));
        } else {
            return null;
        }
    }

    public SonosMetaData getEnqueuedTransportURIMetaData() {

        if (stateMap.get("EnqueuedTransportURIMetaData") != null
                && !stateMap.get("EnqueuedTransportURIMetaData").isEmpty()) {
            return SonosXMLParser.getMetaDataFromXML(stateMap.get("EnqueuedTransportURIMetaData"));
        } else {
            return null;
        }
    }

    public String getMACAddress() {
        updateZoneInfo();
        return stateMap.get("MACAddress");
    }

    public String getPosition() {
        updatePosition();
        return stateMap.get("RelTime");
    }

    public long getCurrenTrackNr() {
        updatePosition();
        String value = stateMap.get("Track");
        if (value != null) {
            return Long.valueOf(value);
        } else {
            return -1;
        }
    }

    public String getVolume() {
        return stateMap.get("VolumeMaster");
    }

    public String getTransportState() {
        return stateMap.get("TransportState");
    }

    public List<SonosEntry> getArtists(String filter) {
        return getEntries("A:", filter);
    }

    public List<SonosEntry> getArtists() {
        return getEntries("A:", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    public List<SonosEntry> getAlbums(String filter) {
        return getEntries("A:ALBUM", filter);
    }

    public List<SonosEntry> getAlbums() {
        return getEntries("A:ALBUM", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    public List<SonosEntry> getTracks(String filter) {
        return getEntries("A:TRACKS", filter);
    }

    public List<SonosEntry> getTracks() {
        return getEntries("A:TRACKS", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    public List<SonosEntry> getQueue(String filter) {
        return getEntries("Q:0", filter);
    }

    public List<SonosEntry> getQueue() {
        return getEntries("Q:0", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    public long getQueueSize() {
        return getNbEntries("Q:0");
    }

    public List<SonosEntry> getPlayLists(String filter) {
        return getEntries("SQ:", filter);
    }

    public List<SonosEntry> getPlayLists() {
        return getEntries("SQ:", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    public List<SonosEntry> getFavoriteRadios(String filter) {
        return getEntries("R:0/0", filter);
    }

    public List<SonosEntry> getFavoriteRadios() {
        return getEntries("R:0/0", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    /**
     * Searches for entries in the 'favorites' list on a sonos account
     *
     * @return
     */
    public List<SonosEntry> getFavorites() {
        return getEntries("FV:2", "dc:title,res,dc:creator,upnp:artist,upnp:album");
    }

    protected List<SonosEntry> getEntries(String type, String filter) {
        long startAt = 0;

        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("ObjectID", type);
        inputs.put("BrowseFlag", "BrowseDirectChildren");
        inputs.put("Filter", filter);
        inputs.put("StartingIndex", Long.toString(startAt));
        inputs.put("RequestedCount", Integer.toString(200));
        inputs.put("SortCriteria", "");

        List<SonosEntry> resultList = null;

        Map<String, String> result = service.invokeAction(this, "ContentDirectory", "Browse", inputs);

        long totalMatches = getResultEntry(result, "TotalMatches", type, filter);
        long initialNumberReturned = getResultEntry(result, "NumberReturned", type, filter);

        String initialResult = result.get("Result");

        resultList = SonosXMLParser.getEntriesFromString(initialResult);
        startAt = startAt + initialNumberReturned;

        while (startAt < totalMatches) {

            inputs.put("StartingIndex", Long.toString(startAt));
            result = service.invokeAction(this, "ContentDirectory", "Browse", inputs);

            // Execute this action synchronously
            String nextResult = result.get("Result");
            long numberReturned = getResultEntry(result, "NumberReturned", type, filter);

            resultList.addAll(SonosXMLParser.getEntriesFromString(nextResult));

            startAt = startAt + numberReturned;
        }

        return resultList;
    }

    protected long getNbEntries(String type) {
        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("ObjectID", type);
        inputs.put("BrowseFlag", "BrowseDirectChildren");
        inputs.put("Filter", "dc:title");
        inputs.put("StartingIndex", "0");
        inputs.put("RequestedCount", "1");
        inputs.put("SortCriteria", "");

        Map<String, String> result = service.invokeAction(this, "ContentDirectory", "Browse", inputs);

        return getResultEntry(result, "TotalMatches", type, "dc:title");
    }

    /**
     * Handles value searching in a SONOS result map (called by {@link #getEntries(String, String)})
     *
     * @param resultInput - the map to be examined for the requestedKey
     * @param requestedKey - the key to be sought in the resultInput map
     * @param entriesType - the 'type' argument of {@link #getEntries(String, String)} method used for logging
     * @param entriesFilter - the 'filter' argument of {@link #getEntries(String, String)} method used for logging
     *
     * @return 0 as long or the value corresponding to the requiredKey if found
     */
    private Long getResultEntry(Map<String, String> resultInput, String requestedKey, String entriesType,
            String entriesFilter) {
        long result = 0;

        if (resultInput.isEmpty()) {
            return result;
        }

        try {
            result = Long.valueOf(resultInput.get(requestedKey));
        } catch (NumberFormatException ex) {
            logger.warn("Could not fetch " + requestedKey + " result for type: " + entriesType + " and filter: "
                    + entriesFilter + ". Using default value '0': " + ex.getMessage(), ex);
        }

        return result;
    }

    /**
     * Save the state (track, position etc) of the Sonos Zone player.
     *
     * @return true if no error occurred.
     */
    protected void saveState() {

        synchronized (stateLock) {

            savedState = new SonosZonePlayerState();
            String currentURI = getCurrentURI();

            savedState.transportState = getTransportState();
            savedState.volume = getVolume();

            if (currentURI != null) {

                if (isPlayingStream(currentURI)) {
                    // we are streaming music
                    SonosMetaData track = getTrackMetadata();
                    SonosMetaData current = getCurrentURIMetadata();
                    if (track != null && current != null) {
                        savedState.entry = new SonosEntry("", current.getTitle(), "", "", track.getAlbumArtUri(),
                                "", current.getUpnpClass(), currentURI);
                    }
                } else if (currentURI.contains(GROUP_URI)) {
                    // we are a slave to some coordinator
                    savedState.entry = new SonosEntry("", "", "", "", "", "", "", currentURI);
                } else if (isPlayingLineIn(currentURI)) {
                    // we are streaming from the Line In connection
                    savedState.entry = new SonosEntry("", "", "", "", "", "", "", currentURI);
                } else if (isPlayingQueue(currentURI)) {
                    // we are playing something that sits in the queue
                    SonosMetaData queued = getEnqueuedTransportURIMetaData();
                    if (queued != null) {

                        savedState.track = getCurrenTrackNr();

                        if (queued.getUpnpClass().contains("object.container.playlistContainer")) {
                            // we are playing a real 'saved' playlist
                            List<SonosEntry> playLists = getPlayLists();
                            for (SonosEntry someList : playLists) {
                                if (someList.getTitle().equals(queued.getTitle())) {
                                    savedState.entry = new SonosEntry(someList.getId(), someList.getTitle(),
                                            someList.getParentId(), "", "", "", someList.getUpnpClass(),
                                            someList.getRes());
                                    break;
                                }
                            }

                        } else if (queued.getUpnpClass().contains("object.container")) {
                            // we are playing some other sort of
                            // 'container' - we will save that to a
                            // playlist for our convenience
                            logger.debug("Save State for a container of type {}", queued.getUpnpClass());

                            // save the playlist
                            String existingList = "";
                            List<SonosEntry> playLists = getPlayLists();
                            for (SonosEntry someList : playLists) {
                                if (someList.getTitle().equals(ESH_PREFIX + getUDN())) {
                                    existingList = someList.getId();
                                    break;
                                }
                            }

                            saveQueue(ESH_PREFIX + getUDN(), existingList);

                            // get all the playlists and a ref to our
                            // saved list
                            playLists = getPlayLists();
                            for (SonosEntry someList : playLists) {
                                if (someList.getTitle().equals(ESH_PREFIX + getUDN())) {
                                    savedState.entry = new SonosEntry(someList.getId(), someList.getTitle(),
                                            someList.getParentId(), "", "", "", someList.getUpnpClass(),
                                            someList.getRes());
                                    break;
                                }
                            }

                        }
                    } else {
                        savedState.entry = new SonosEntry("", "", "", "", "", "", "", QUEUE_URI + getUDN() + "#0");
                    }
                }

                savedState.relTime = getPosition();
            } else {
                savedState.entry = null;
            }
        }
    }

    /**
     * Restore the state (track, position etc) of the Sonos Zone player.
     *
     * @return true if no error occurred.
     */
    protected void restoreState() {

        synchronized (stateLock) {
            if (savedState != null) {
                // put settings back
                if (savedState.volume != null) {
                    setVolume(DecimalType.valueOf(savedState.volume));
                }

                if (isCoordinator()) {
                    if (savedState.entry != null) {
                        // check if we have a playlist to deal with
                        if (savedState.entry.getUpnpClass().contains("object.container.playlistContainer")) {

                            addURIToQueue(savedState.entry.getRes(),
                                    SonosXMLParser.compileMetadataString(savedState.entry), 0, true);
                            SonosEntry entry = new SonosEntry("", "", "", "", "", "", "",
                                    QUEUE_URI + getUDN() + "#0");
                            setCurrentURI(entry);
                            setPositionTrack(savedState.track);

                        } else {
                            setCurrentURI(savedState.entry);
                            setPosition(savedState.relTime);
                        }
                    }

                    if (savedState.transportState != null) {
                        if (savedState.transportState.equals("PLAYING")) {
                            play();
                        } else if (savedState.transportState.equals("STOPPED")) {
                            stop();
                        } else if (savedState.transportState.equals("PAUSED_PLAYBACK")) {
                            pause();
                        }
                    }
                }
            }
        }
    }

    public void saveQueue(String name, String queueID) {

        if (name != null && queueID != null) {

            Map<String, String> inputs = new HashMap<String, String>();
            inputs.put("Title", name);
            inputs.put("ObjectID", queueID);

            Map<String, String> result = service.invokeAction(this, "AVTransport", "SaveQueue", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        }
    }

    public void setVolume(Command command) {
        if (command != null) {
            if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
                    || command instanceof DecimalType || command instanceof PercentType) {

                Map<String, String> inputs = new HashMap<String, String>();

                String newValue = null;
                if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.INCREASE) {
                    int i = Integer.valueOf(this.getVolume());
                    newValue = String.valueOf(Math.min(100, i + 1));
                } else if (command instanceof IncreaseDecreaseType && command == IncreaseDecreaseType.DECREASE) {
                    int i = Integer.valueOf(this.getVolume());
                    newValue = String.valueOf(Math.max(0, i - 1));
                } else if (command instanceof OnOffType && command == OnOffType.ON) {
                    newValue = "100";
                } else if (command instanceof OnOffType && command == OnOffType.OFF) {
                    newValue = "0";
                } else if (command instanceof DecimalType) {
                    newValue = command.toString();
                } else {
                    return;
                }
                inputs.put("Channel", "Master");
                inputs.put("DesiredVolume", newValue);

                Map<String, String> result = service.invokeAction(this, "RenderingControl", "SetVolume", inputs);

                for (String variable : result.keySet()) {
                    this.onValueReceived(variable, result.get(variable), "RenderingControl");
                }
            }
        }
    }

    /**
     * Set the VOLUME command specific to the current grouping according to the Sonos behaviour.
     * AdHoc groups handles the volume specifically for each player.
     * Bonded groups delegate the volume to the coordinator which applies the same level to all group members.
     */
    public void setVolumeForGroup(Command command) {
        if (isAdHocGroup() || isStandalonePlayer()) {
            setVolume(command);
        } else {
            try {
                getCoordinatorHandler().setVolume(command);
            } catch (IllegalStateException e) {
                logger.warn("Cannot set group volume ({})", e.getMessage());
            }
        }
    }

    /**
     * Checks if the player receiving the command is part of a group that
     * consists of randomly added players or contains bonded players
     *
     * @return boolean
     */
    private boolean isAdHocGroup() {
        SonosZoneGroup currentZoneGroup = getCurrentZoneGroup();
        if (currentZoneGroup != null) {
            List<String> zoneGroupMemberNames = currentZoneGroup.getMemberZoneNames();

            if (zoneGroupMemberNames != null) {
                for (String zoneName : zoneGroupMemberNames) {
                    if (!zoneName.equals(zoneGroupMemberNames.get(0))) {
                        // At least one "ZoneName" differs so we have an AdHoc group
                        return true;
                    }
                }
            }
        }
        return false;
    }

    /**
     * Checks if the player receiving the command is a standalone player
     *
     * @return boolean
     */
    private boolean isStandalonePlayer() {
        return getCurrentZoneGroup() != null ? getCurrentZoneGroup().getMembers().size() == 1 : true;
    }

    /**
     * Returns the current zone group
     * (of which the player receiving the command is part)
     *
     * @return {@link SonosZoneGroup}
     */
    private SonosZoneGroup getCurrentZoneGroup() {
        String zoneGroupState = stateMap.get("ZoneGroupState");
        if (zoneGroupState != null) {
            Collection<SonosZoneGroup> zoneGroups = SonosXMLParser.getZoneGroupFromXML(zoneGroupState);

            for (SonosZoneGroup zoneGroup : zoneGroups) {
                if (zoneGroup.getMembers().contains(getUDN())) {
                    return zoneGroup;
                }
            }
        }
        logger.debug("Could not fetch Sonos group state information");
        return null;
    }

    /**
     * Sets the volume level for a notification sound
     * (initializes {@link #notificationSoundVolume})
     *
     * @param command
     */
    public void setNotificationSoundVolume(Command command) {
        if (command != null) {
            notificationSoundVolume = command.toString();
        }
    }

    /**
     * Gets the volume level for a notification sound
     */
    public PercentType getNotificationSoundVolume() {
        if (notificationSoundVolume == null) {
            // we need to initialize the value for the first time
            notificationSoundVolume = getVolume();
            if (notificationSoundVolume != null) {
                updateState(SonosBindingConstants.NOTIFICATIONVOLUME,
                        new PercentType(new BigDecimal(notificationSoundVolume)));
            }
        }
        if (notificationSoundVolume != null) {
            return new PercentType(new BigDecimal(notificationSoundVolume));
        } else {
            return null;
        }
    }

    public void addURIToQueue(String URI, String meta, long desiredFirstTrack, boolean enqueueAsNext) {

        if (URI != null && meta != null) {

            Map<String, String> inputs = new HashMap<String, String>();

            try {
                inputs.put("InstanceID", "0");
                inputs.put("EnqueuedURI", URI);
                inputs.put("EnqueuedURIMetaData", meta);
                inputs.put("DesiredFirstTrackNumberEnqueued", Long.toString(desiredFirstTrack));
                inputs.put("EnqueueAsNext", Boolean.toString(enqueueAsNext));
            } catch (NumberFormatException ex) {
                logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
            }

            Map<String, String> result = service.invokeAction(this, "AVTransport", "AddURIToQueue", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        }
    }

    public void setCurrentURI(SonosEntry newEntry) {
        setCurrentURI(newEntry.getRes(), SonosXMLParser.compileMetadataString(newEntry));
    }

    public void setCurrentURI(String URI, String URIMetaData) {
        if (URI != null && URIMetaData != null) {

            Map<String, String> inputs = new HashMap<String, String>();

            try {
                inputs.put("InstanceID", "0");
                inputs.put("CurrentURI", URI);
                inputs.put("CurrentURIMetaData", URIMetaData);
            } catch (NumberFormatException ex) {
                logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
            }

            Map<String, String> result = service.invokeAction(this, "AVTransport", "SetAVTransportURI", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        }
    }

    public void setPosition(String relTime) {
        seek("REL_TIME", relTime);
    }

    public void setPositionTrack(long tracknr) {
        seek("TRACK_NR", Long.toString(tracknr));
    }

    public void setPositionTrack(String tracknr) {
        seek("TRACK_NR", tracknr);
    }

    protected void seek(String unit, String target) {
        if (unit != null && target != null) {

            Map<String, String> inputs = new HashMap<String, String>();

            try {
                inputs.put("InstanceID", "0");
                inputs.put("Unit", unit);
                inputs.put("Target", target);
            } catch (NumberFormatException ex) {
                logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
            }

            Map<String, String> result = service.invokeAction(this, "AVTransport", "Seek", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        }
    }

    public void play() {

        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("Speed", "1");

        Map<String, String> result = service.invokeAction(this, "AVTransport", "Play", inputs);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void stop() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "Stop", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void pause() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "Pause", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void setShuffle(Command command) {
        if ((command != null) && (command instanceof OnOffType || command instanceof OpenClosedType
                || command instanceof UpDownType)) {

            try {
                ZonePlayerHandler coordinator = getCoordinatorHandler();

                if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
                        || command.equals(OpenClosedType.OPEN)) {
                    switch (coordinator.getRepeatMode()) {
                    case "ALL":
                        coordinator.updatePlayMode("SHUFFLE");
                        break;
                    case "ONE":
                        coordinator.updatePlayMode("SHUFFLE_REPEAT_ONE");
                        break;
                    case "OFF":
                        coordinator.updatePlayMode("SHUFFLE_NOREPEAT");
                        break;
                    }
                } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)
                        || command.equals(OpenClosedType.CLOSED)) {
                    switch (coordinator.getRepeatMode()) {
                    case "ALL":
                        coordinator.updatePlayMode("REPEAT_ALL");
                        break;
                    case "ONE":
                        coordinator.updatePlayMode("REPEAT_ONE");
                        break;
                    case "OFF":
                        coordinator.updatePlayMode("NORMAL");
                        break;
                    }
                }

            } catch (IllegalStateException e) {
                logger.warn("Cannot handle shuffle command ({})", e.getMessage());
            }
        }
    }

    public void setRepeat(Command command) {
        if ((command != null) && (command instanceof StringType)) {
            try {
                ZonePlayerHandler coordinator = getCoordinatorHandler();

                switch (command.toString()) {
                case "ALL":
                    coordinator.updatePlayMode(coordinator.isShuffleActive() ? "SHUFFLE" : "REPEAT_ALL");
                    break;
                case "ONE":
                    coordinator.updatePlayMode(coordinator.isShuffleActive() ? "SHUFFLE_REPEAT_ONE" : "REPEAT_ONE");
                    break;
                case "OFF":
                    coordinator.updatePlayMode(coordinator.isShuffleActive() ? "SHUFFLE_NOREPEAT" : "NORMAL");
                    break;
                default:
                    logger.warn("{}: unexpected repeat command; accepted values are ALL, ONE and OFF",
                            command.toString());
                    break;
                }

            } catch (IllegalStateException e) {
                logger.warn("Cannot handle repeat command ({})", e.getMessage());
            }
        }
    }

    public Boolean isShuffleActive() {
        return ((stateMap.get("CurrentPlayMode") != null) && stateMap.get("CurrentPlayMode").startsWith("SHUFFLE"))
                ? true
                : false;
    }

    public String getRepeatMode() {
        String mode = "OFF";
        if (stateMap.get("CurrentPlayMode") != null) {
            switch (stateMap.get("CurrentPlayMode")) {
            case "REPEAT_ALL":
            case "SHUFFLE":
                mode = "ALL";
                break;
            case "REPEAT_ONE":
            case "SHUFFLE_REPEAT_ONE":
                mode = "ONE";
                break;
            case "NORMAL":
            case "SHUFFLE_NOREPEAT":
            default:
                mode = "OFF";
                break;
            }
        }
        return mode;
    }

    protected void updatePlayMode(String playMode) {
        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("InstanceID", "0");
        inputs.put("NewPlayMode", playMode);

        Map<String, String> result = service.invokeAction(this, "AVTransport", "SetPlayMode", inputs);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    /**
     * Clear all scheduled music from the current queue.
     *
     */
    public void removeAllTracksFromQueue() {
        Map<String, String> inputs = new HashMap<String, String>();
        inputs.put("InstanceID", "0");

        Map<String, String> result = service.invokeAction(this, "AVTransport", "RemoveAllTracksFromQueue", inputs);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    /**
     * Play music from the line-in of the given Player referenced by the given UDN or name
     *
     * @param udn or name
     */
    public void playLineIn(Command command) {

        if (command != null && command instanceof StringType) {

            try {
                String remotePlayerName = command.toString();
                ZonePlayerHandler coordinatorHandler = getCoordinatorHandler();
                ZonePlayerHandler remoteHandler = getHandlerByName(remotePlayerName);

                // check if player has a line-in connected
                if (remoteHandler.isAnalogLineInConnected() || remoteHandler.isOpticalLineInConnected()) {

                    // stop whatever is currently playing
                    coordinatorHandler.stop();

                    // set the URI
                    if (remoteHandler.isAnalogLineInConnected()) {
                        coordinatorHandler.setCurrentURI(ANALOG_LINE_IN_URI + remoteHandler.getUDN(), "");
                    } else {
                        coordinatorHandler.setCurrentURI(OPTICAL_LINE_IN_URI + remoteHandler.getUDN() + SPDIF, "");
                    }

                    // take the system off mute
                    coordinatorHandler.setMute(OnOffType.OFF);

                    // start jammin'
                    coordinatorHandler.play();
                } else {
                    logger.warn("Line-in of {} is not connected", remoteHandler.getUDN());
                }

            } catch (IllegalStateException e) {
                logger.warn("Cannot play line-in ({})", e.getMessage());
            }
        }
    }

    private ZonePlayerHandler getCoordinatorHandler() throws IllegalStateException {
        if (coordinatorHandler == null) {
            try {
                coordinatorHandler = getHandlerByName(getCoordinator());
            } catch (IllegalStateException e) {
                coordinatorHandler = null;
                throw new IllegalStateException("Missing group coordinator " + getCoordinator());
            }
        }
        return coordinatorHandler;
    }

    /**
     * Returns a list of all zone group members this particular player is member of
     * Or empty list if the players is not assigned to any group
     *
     * @return a list of Strings containing the UDNs of other group members
     */
    protected List<String> getZoneGroupMembers() {
        List<String> result = new ArrayList<>();

        if (stateMap.get("ZoneGroupState") != null) {
            Collection<SonosZoneGroup> zoneGroups = SonosXMLParser
                    .getZoneGroupFromXML(stateMap.get("ZoneGroupState"));

            for (SonosZoneGroup zg : zoneGroups) {
                if (zg.getMembers().contains(getUDN())) {
                    result.addAll(zg.getMembers());
                    break;
                }
            }
        } else {
            // If the group topology was not yet received, return at least the current Sonos zone
            result.add(getUDN());
        }
        return result;
    }

    /**
     * Returns a list of other zone group members this particular player is member of
     * Or empty list if the players is not assigned to any group
     *
     * @return a list of Strings containing the UDNs of other group members
     */
    protected List<String> getOtherZoneGroupMembers() {
        List<String> zoneGroupMembers = getZoneGroupMembers();
        zoneGroupMembers.remove(getUDN());
        return zoneGroupMembers;
    }

    protected ZonePlayerHandler getHandlerByName(String remotePlayerName) throws IllegalStateException {
        if (thingRegistry != null) {
            for (ThingTypeUID supportedThingType : SonosBindingConstants.SUPPORTED_THING_TYPES_UIDS) {
                Thing thing = thingRegistry.get(new ThingUID(supportedThingType, remotePlayerName));
                if (thing != null) {
                    return (ZonePlayerHandler) thing.getHandler();
                }
            }
            Collection<Thing> allThings = thingRegistry.getAll();
            for (Thing aThing : allThings) {
                if (SonosBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(aThing.getThingTypeUID())
                        && aThing.getConfiguration().get(ZonePlayerConfiguration.UDN).equals(remotePlayerName)) {
                    return (ZonePlayerHandler) aThing.getHandler();
                }
            }
        }
        throw new IllegalStateException("Could not find handler for " + remotePlayerName);
    }

    public void setMute(Command command) {
        if (command != null) {
            if (command instanceof OnOffType || command instanceof OpenClosedType
                    || command instanceof UpDownType) {

                Map<String, String> inputs = new HashMap<String, String>();
                inputs.put("Channel", "Master");

                if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
                        || command.equals(OpenClosedType.OPEN)) {
                    inputs.put("DesiredMute", "True");
                } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)
                        || command.equals(OpenClosedType.CLOSED)) {
                    inputs.put("DesiredMute", "False");

                }

                Map<String, String> result = service.invokeAction(this, "RenderingControl", "SetMute", inputs);

                for (String variable : result.keySet()) {
                    this.onValueReceived(variable, result.get(variable), "RenderingControl");
                }
            }
        }
    }

    public List<SonosAlarm> getCurrentAlarmList() {
        Map<String, String> result = service.invokeAction(this, "AlarmClock", "ListAlarms", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AlarmClock");
        }

        return SonosXMLParser.getAlarmsFromStringResult(result.get("CurrentAlarmList"));
    }

    public void updateAlarm(SonosAlarm alarm) {
        if (alarm != null) {

            Map<String, String> inputs = new HashMap<String, String>();

            try {
                inputs.put("ID", Integer.toString(alarm.getID()));
                inputs.put("StartLocalTime", alarm.getStartTime());
                inputs.put("Duration", alarm.getDuration());
                inputs.put("Recurrence", alarm.getRecurrence());
                inputs.put("RoomUUID", alarm.getRoomUUID());
                inputs.put("ProgramURI", alarm.getProgramURI());
                inputs.put("ProgramMetaData", alarm.getProgramMetaData());
                inputs.put("PlayMode", alarm.getPlayMode());
                inputs.put("Volume", Integer.toString(alarm.getVolume()));
                if (alarm.getIncludeLinkedZones()) {
                    inputs.put("IncludeLinkedZones", "1");
                } else {
                    inputs.put("IncludeLinkedZones", "0");
                }

                if (alarm.getEnabled()) {
                    inputs.put("Enabled", "1");
                } else {
                    inputs.put("Enabled", "0");
                }
            } catch (NumberFormatException ex) {
                logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
            }

            Map<String, String> result = service.invokeAction(this, "AlarmClock", "UpdateAlarm", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AlarmClock");
            }
        }
    }

    public void setAlarm(Command command) {
        if (command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType) {
            if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
                    || command.equals(OpenClosedType.OPEN)) {
                setAlarm(true);
            } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)
                    || command.equals(OpenClosedType.CLOSED)) {
                setAlarm(false);
            }
        }
    }

    public void setAlarm(boolean alarmSwitch) {

        List<SonosAlarm> sonosAlarms = getCurrentAlarmList();

        // find the nearest alarm - take the current time from the Sonos system,
        // not the system where we are running
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        fmt.setTimeZone(TimeZone.getTimeZone("GMT"));

        String currentLocalTime = getTime();
        Date currentDateTime = null;
        try {
            currentDateTime = fmt.parse(currentLocalTime);
        } catch (ParseException e) {
            logger.error("An exception occurred while formatting a date");
            e.printStackTrace();
        }

        if (currentDateTime != null) {
            Calendar currentDateTimeCalendar = Calendar.getInstance();
            currentDateTimeCalendar.setTimeZone(TimeZone.getTimeZone("GMT"));
            currentDateTimeCalendar.setTime(currentDateTime);
            currentDateTimeCalendar.add(Calendar.DAY_OF_YEAR, 10);
            long shortestDuration = currentDateTimeCalendar.getTimeInMillis() - currentDateTime.getTime();

            SonosAlarm firstAlarm = null;

            for (SonosAlarm anAlarm : sonosAlarms) {
                SimpleDateFormat durationFormat = new SimpleDateFormat("HH:mm:ss");
                durationFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
                Date durationDate;
                try {
                    durationDate = durationFormat.parse(anAlarm.getDuration());
                } catch (ParseException e) {
                    logger.error("An exception occurred while parsing a date : '{}'", e.getMessage());
                    continue;
                }

                long duration = durationDate.getTime();

                if (duration < shortestDuration && anAlarm.getRoomUUID().equals(getUDN())) {
                    shortestDuration = duration;
                    firstAlarm = anAlarm;
                }
            }

            // Set the Alarm
            if (firstAlarm != null) {

                if (alarmSwitch) {
                    firstAlarm.setEnabled(true);
                } else {
                    firstAlarm.setEnabled(false);
                }

                updateAlarm(firstAlarm);

            }
        }
    }

    public String getTime() {
        updateTime();
        return stateMap.get("CurrentLocalTime");
    }

    public Boolean isAlarmRunning() {
        return ((stateMap.get("AlarmRunning") != null) && stateMap.get("AlarmRunning").equals("1")) ? true : false;
    }

    public void snoozeAlarm(Command command) {
        if (isAlarmRunning() && command instanceof DecimalType) {

            int minutes = ((DecimalType) command).intValue();

            Map<String, String> inputs = new HashMap<String, String>();

            Calendar snoozePeriod = Calendar.getInstance();
            snoozePeriod.setTimeZone(TimeZone.getTimeZone("GMT"));
            snoozePeriod.setTimeInMillis(0);
            snoozePeriod.add(Calendar.MINUTE, minutes);
            SimpleDateFormat pFormatter = new SimpleDateFormat("HH:mm:ss");
            pFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));

            try {
                inputs.put("Duration", pFormatter.format(snoozePeriod.getTime()));
            } catch (NumberFormatException ex) {
                logger.error("Action Invalid Value Format Exception {}", ex.getMessage());
            }

            Map<String, String> result = service.invokeAction(this, "AVTransport", "SnoozeAlarm", inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        } else {
            logger.warn("There is no alarm running on {}", getUDN());
        }
    }

    public Boolean isAnalogLineInConnected() {
        return ((stateMap.get("LineInConnected") != null) && stateMap.get("LineInConnected").equals("true")) ? true
                : false;
    }

    public Boolean isOpticalLineInConnected() {
        return ((stateMap.get("TOSLinkConnected") != null) && stateMap.get("TOSLinkConnected").equals("true"))
                ? true
                : false;
    }

    public void becomeStandAlonePlayer() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "BecomeCoordinatorOfStandaloneGroup",
                null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void addMember(Command command) {
        if (command != null && command instanceof StringType) {
            SonosEntry entry = new SonosEntry("", "", "", "", "", "", "", GROUP_URI + getUDN());
            try {
                getHandlerByName(command.toString()).setCurrentURI(entry);
            } catch (IllegalStateException e) {
                logger.warn("Cannot add group member ({})", e.getMessage());
            }
        }
    }

    public boolean publicAddress() {
        // check if sourcePlayer has a line-in connected
        if (isAnalogLineInConnected() || isOpticalLineInConnected()) {

            // first remove this player from its own group if any
            becomeStandAlonePlayer();

            List<SonosZoneGroup> currentSonosZoneGroups = new ArrayList<SonosZoneGroup>();
            for (SonosZoneGroup grp : SonosXMLParser.getZoneGroupFromXML(stateMap.get("ZoneGroupState"))) {
                currentSonosZoneGroups.add((SonosZoneGroup) grp.clone());
            }

            // add all other players to this new group
            for (SonosZoneGroup group : currentSonosZoneGroups) {
                for (String player : group.getMembers()) {
                    try {
                        ZonePlayerHandler somePlayer = getHandlerByName(player);
                        if (somePlayer != this) {
                            somePlayer.becomeStandAlonePlayer();
                            somePlayer.stop();
                            addMember(StringType.valueOf(somePlayer.getUDN()));
                        }
                    } catch (IllegalStateException e) {
                        logger.warn("Cannot add to group ({})", e.getMessage());
                    }
                }
            }

            try {
                ZonePlayerHandler coordinator = getCoordinatorHandler();
                // set the URI of the group to the line-in
                SonosEntry entry = new SonosEntry("", "", "", "", "", "", "", ANALOG_LINE_IN_URI + getUDN());
                if (isOpticalLineInConnected()) {
                    entry = new SonosEntry("", "", "", "", "", "", "", OPTICAL_LINE_IN_URI + getUDN() + SPDIF);
                }
                coordinator.setCurrentURI(entry);
                coordinator.play();

                return true;

            } catch (IllegalStateException e) {
                logger.warn("Cannot handle command ({})", e.getMessage());
                return false;
            }

        } else {
            logger.warn("Line-in of {} is not connected", getUDN());
            return false;
        }

    }

    /**
     * Play a given url to music in one of the music libraries.
     *
     * @param url
     *            in the format of //host/folder/filename.mp3
     */
    public void playURI(Command command) {

        if (command != null && command instanceof StringType) {

            try {
                String url = command.toString();

                ZonePlayerHandler coordinator = getCoordinatorHandler();

                // stop whatever is currently playing
                coordinator.stop();
                coordinator.waitForNotTransportState("PLAYING");

                // clear any tracks which are pending in the queue
                coordinator.removeAllTracksFromQueue();

                // add the new track we want to play to the queue
                // The url will be prefixed with x-file-cifs if it is NOT a http URL
                if (!url.startsWith("x-") && (!url.startsWith("http"))) {
                    // default to file based url
                    url = FILE_URI + url;
                }
                coordinator.addURIToQueue(url, "", 0, true);

                // set the current playlist to our new queue
                coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");

                // take the system off mute
                coordinator.setMute(OnOffType.OFF);

                // start jammin'
                coordinator.play();

            } catch (IllegalStateException e) {
                logger.warn("Cannot play URI ({})", e.getMessage());
            }
        }

    }

    private void scheduleNotificationSound(final Command command) {
        scheduler.schedule(new Runnable() {

            @Override
            public void run() {
                synchronized (notificationLock) {
                    playNotificationSoundURI(command);
                }
            }
        }, 0, TimeUnit.MILLISECONDS);
    }

    /**
     * Play a given notification sound
     *
     * @param url in the format of //host/folder/filename.mp3
     */
    public void playNotificationSoundURI(Command notificationURL) {

        if (notificationURL != null && notificationURL instanceof StringType) {
            try {
                ZonePlayerHandler coordinator = getCoordinatorHandler();

                String currentURI = coordinator.getCurrentURI();

                if (isPlayingStream(currentURI)) {
                    handleRadioStream(currentURI, notificationURL, coordinator);
                } else if (isPlayingLineIn(currentURI)) {
                    handleLineIn(currentURI, notificationURL, coordinator);
                } else if (isPlayingQueue(currentURI)) {
                    handleSharedQueue(notificationURL, coordinator);
                } else if (isPlaylistEmpty(coordinator)) {
                    handleEmptyQueue(notificationURL, coordinator);
                }
                synchronized (notificationLock) {
                    notificationLock.notify();
                }

            } catch (IllegalStateException e) {
                logger.warn("Cannot play sound ({})", e.getMessage());
            }
        }
    }

    private boolean isPlaylistEmpty(ZonePlayerHandler coordinator) {
        return coordinator.getQueueSize() == 0;
    }

    private boolean isPlayingQueue(String currentURI) {
        if (currentURI == null) {
            return false;
        }
        return currentURI.contains(QUEUE_URI);
    }

    private boolean isPlayingStream(String currentURI) {
        if (currentURI == null) {
            return false;
        }
        return currentURI.contains(STREAM_URI);
    }

    private boolean isPlayingLineIn(String currentURI) {
        if (currentURI == null) {
            return false;
        }
        return currentURI.contains(ANALOG_LINE_IN_URI)
                || (currentURI.startsWith(OPTICAL_LINE_IN_URI) && currentURI.endsWith(SPDIF));

    }

    /**
     * Does a chain of predefined actions when a Notification sound is played by
     * {@link ZonePlayerHandler#playNotificationSoundURI(Command)} in case
     * radio streaming is currently loaded
     *
     * @param currentStreamURI - the currently loaded stream's URI
     * @param notificationURL - the notification url in the format of //host/folder/filename.mp3
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void handleRadioStream(String currentStreamURI, Command notificationURL,
            ZonePlayerHandler coordinator) {
        String nextAction = coordinator.getTransportState();
        SonosMetaData track = coordinator.getTrackMetadata();
        SonosMetaData currentURI = coordinator.getCurrentURIMetadata();

        if (track != null && currentURI != null) {
            handleNotificationSound(notificationURL, coordinator);
            coordinator.setCurrentURI(new SonosEntry("", currentURI.getTitle(), "", "", track.getAlbumArtUri(), "",
                    currentURI.getUpnpClass(), currentStreamURI));

            restoreLastTransportState(coordinator, nextAction);
        }
    }

    /**
     * Does a chain of predefined actions when a Notification sound is played by
     * {@link ZonePlayerHandler#playNotificationSoundURI(Command)} in case
     * line in is currently loaded
     *
     * @param currentLineInURI - the currently loaded line-in URI
     * @param notificationURL - the notification url in the format of //host/folder/filename.mp3
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void handleLineIn(String currentLineInURI, Command notificationURL, ZonePlayerHandler coordinator) {
        String nextAction = coordinator.getTransportState();

        handleNotificationSound(notificationURL, coordinator);
        coordinator.setCurrentURI(currentLineInURI, "");

        restoreLastTransportState(coordinator, nextAction);
    }

    /**
     * Does a chain of predefined actions when a Notification sound is played by
     * {@link ZonePlayerHandler#playNotificationSoundURI(Command)} in case
     * shared queue is currently loaded
     *
     * @param notificationURL - the notification url in the format of //host/folder/filename.mp3
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void handleSharedQueue(Command notificationURL, ZonePlayerHandler coordinator) {
        String nextAction = coordinator.getTransportState();
        String trackPosition = coordinator.getPosition();
        long currentTrackNumber = coordinator.getCurrenTrackNr();

        handleNotificationSound(notificationURL, coordinator);
        coordinator.setPositionTrack(currentTrackNumber);
        coordinator.setPosition(trackPosition);

        restoreLastTransportState(coordinator, nextAction);
    }

    /**
     * Handle the execution of the notification sound by sequentially executing the required steps.
     *
     * @param notificationURL - the notification url in the format of //host/folder/filename.mp3
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void handleNotificationSound(Command notificationURL, ZonePlayerHandler coordinator) {
        String originalVolume = (isAdHocGroup() || isStandalonePlayer()) ? getVolume() : coordinator.getVolume();
        coordinator.stop();
        coordinator.waitForNotTransportState("PLAYING");
        applyNotificationSoundVolume();
        long notificationPosition = coordinator.getQueueSize() + 1;
        coordinator.addURIToQueue(notificationURL.toString(), "", notificationPosition, false);
        coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");
        coordinator.setPositionTrack(notificationPosition);
        coordinator.play();
        coordinator.waitForFinishedNotification();
        if (originalVolume != null) {
            setVolumeForGroup(DecimalType.valueOf(originalVolume));
        }
        coordinator.removeRangeOfTracksFromQueue(new StringType(Long.toString(notificationPosition) + ",1"));
    }

    private void restoreLastTransportState(ZonePlayerHandler coordinator, String nextAction) {
        if (nextAction != null) {
            switch (nextAction) {
            case "PLAYING":
                coordinator.play();
                coordinator.waitForTransportState("PLAYING");
                break;
            case "PAUSED_PLAYBACK":
                coordinator.pause();
                break;
            }
        }
    }

    /**
     * Does a chain of predefined actions when a Notification sound is played by
     * {@link ZonePlayerHandler#playNotificationSoundURI(Command)} in case
     * empty queue is currently loaded
     *
     * @param notificationURL - the notification url in the format of //host/folder/filename.mp3
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void handleEmptyQueue(Command notificationURL, ZonePlayerHandler coordinator) {
        String originalVolume = coordinator.getVolume();
        coordinator.applyNotificationSoundVolume();
        coordinator.playURI(notificationURL);
        coordinator.waitForFinishedNotification();
        coordinator.removeAllTracksFromQueue();
        coordinator.setVolume(DecimalType.valueOf(originalVolume));
    }

    /**
     * Applies the notification sound volume level to the group (if not null)
     *
     * @param coordinator - {@link ZonePlayerHandler} coordinator for the SONOS device(s)
     */
    private void applyNotificationSoundVolume() {
        PercentType volume = getNotificationSoundVolume();
        if (volume != null) {
            setVolumeForGroup(volume);
        }
    }

    private void waitForFinishedNotification() {
        waitForTransportState("PLAYING");

        // check Sonos state events to determine the end of the notification sound
        String notificationTitle = stateMap.get("CurrentTitle");
        long playstart = System.currentTimeMillis();
        while (System.currentTimeMillis() - playstart < NOTIFICATION_TIMEOUT) {
            try {
                Thread.sleep(50);
                if (!notificationTitle.equals(stateMap.get("CurrentTitle"))
                        || !"PLAYING".equals(stateMap.get("TransportState"))) {
                    break;
                }
            } catch (InterruptedException e) {
                logger.error("InterruptedException during playing a notification sound");
            }
        }
    }

    private void waitForTransportState(String state) {
        if (stateMap.get("TransportState") != null) {
            long start = System.currentTimeMillis();
            while (!stateMap.get("TransportState").equals(state)) {
                try {
                    Thread.sleep(50);
                    if (System.currentTimeMillis() - start > NOTIFICATION_TIMEOUT) {
                        break;
                    }
                } catch (InterruptedException e) {
                    logger.error("InterruptedException during playing a notification sound");
                }
            }
        }
    }

    private void waitForNotTransportState(String state) {
        if (stateMap.get("TransportState") != null) {
            long start = System.currentTimeMillis();
            while (stateMap.get("TransportState").equals(state)) {
                try {
                    Thread.sleep(50);
                    if (System.currentTimeMillis() - start > NOTIFICATION_TIMEOUT) {
                        break;
                    }
                } catch (InterruptedException e) {
                    logger.error("InterruptedException during playing a notification sound");
                }
            }
        }
    }

    /**
     * Removes a range of tracks from the queue.
     * (<x,y> will remove y songs started by the song number x)
     *
     * @param command - must be in the format <startIndex, numberOfSongs>
     */
    public void removeRangeOfTracksFromQueue(Command command) {
        if (command != null && command instanceof StringType) {
            Map<String, String> inputs = new HashMap<String, String>();
            String[] rangeInputSplit = command.toString().split(",");

            // If range input is incorrect, remove the first song by default
            String startIndex = rangeInputSplit[0] != null ? rangeInputSplit[0] : "1";
            String numberOfTracks = rangeInputSplit[1] != null ? rangeInputSplit[1] : "1";

            inputs.put("InstanceID", "0");
            inputs.put("StartingIndex", startIndex);
            inputs.put("NumberOfTracks", numberOfTracks);

            Map<String, String> result = service.invokeAction(this, "AVTransport", "RemoveTrackRangeFromQueue",
                    inputs);

            for (String variable : result.keySet()) {
                this.onValueReceived(variable, result.get(variable), "AVTransport");
            }
        }
    }

    public void playQueue() {
        try {
            ZonePlayerHandler coordinator = getCoordinatorHandler();

            // set the current playlist to our new queue
            coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");

            // take the system off mute
            coordinator.setMute(OnOffType.OFF);

            // start jammin'
            coordinator.play();

        } catch (IllegalStateException e) {
            logger.warn("Cannot play queue ({})", e.getMessage());
        }
    }

    public void setLed(Command command) {
        if (command != null) {
            if (command instanceof OnOffType || command instanceof OpenClosedType
                    || command instanceof UpDownType) {

                Map<String, String> inputs = new HashMap<String, String>();

                if (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
                        || command.equals(OpenClosedType.OPEN)) {
                    inputs.put("DesiredLEDState", "On");
                } else if (command.equals(OnOffType.OFF) || command.equals(UpDownType.DOWN)
                        || command.equals(OpenClosedType.CLOSED)) {
                    inputs.put("DesiredLEDState", "Off");

                }

                Map<String, String> result = service.invokeAction(this, "DeviceProperties", "SetLEDState", inputs);
                Map<String, String> result2 = service.invokeAction(this, "DeviceProperties", "GetLEDState", null);

                result.putAll(result2);

                for (String variable : result.keySet()) {
                    this.onValueReceived(variable, result.get(variable), "DeviceProperties");
                }
            }
        }
    }

    public void removeMember(Command command) {
        if (command != null && command instanceof StringType) {
            try {
                ZonePlayerHandler oldmemberHandler = getHandlerByName(command.toString());

                oldmemberHandler.becomeStandAlonePlayer();
                SonosEntry entry = new SonosEntry("", "", "", "", "", "", "",
                        QUEUE_URI + oldmemberHandler.getUDN() + "#0");
                oldmemberHandler.setCurrentURI(entry);
            } catch (IllegalStateException e) {
                logger.warn("Cannot remove group member ({})", e.getMessage());
            }
        }
    }

    public void previous() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "Previous", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void next() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "Next", null);

        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    public void playRadio(Command command) {

        if (command instanceof StringType) {
            String station = command.toString();
            List<SonosEntry> stations = getFavoriteRadios();

            SonosEntry theEntry = null;
            // search for the appropriate radio based on its name (title)
            for (SonosEntry someStation : stations) {
                if (someStation.getTitle().equals(station)) {
                    theEntry = someStation;
                    break;
                }
            }

            // set the URI of the group coordinator
            if (theEntry != null) {
                try {
                    ZonePlayerHandler coordinator = getCoordinatorHandler();
                    coordinator.setCurrentURI(theEntry);
                    coordinator.play();
                } catch (IllegalStateException e) {
                    logger.warn("Cannot play radio ({})", e.getMessage());
                }
            } else {
                logger.warn("Radio station '{}' not found", station);
            }
        }

    }

    /**
     * This will attempt to match the station string with a entry in the
     * favorites list, this supports both single entries and playlists
     *
     * @param favorite to match
     * @return true if a match was found and played.
     */
    public void playFavorite(Command command) {

        if (command instanceof StringType) {
            String favorite = command.toString();
            List<SonosEntry> favorites = getFavorites();

            SonosEntry theEntry = null;
            // search for the appropriate favorite based on its name (title)
            for (SonosEntry entry : favorites) {
                if (entry.getTitle().equals(favorite)) {
                    theEntry = entry;
                    break;
                }
            }

            // set the URI of the group coordinator
            if (theEntry != null) {
                try {
                    ZonePlayerHandler coordinator = getCoordinatorHandler();

                    /**
                     * If this is a playlist we need to treat it as such
                     */
                    if (theEntry.getResourceMetaData() != null
                            && theEntry.getResourceMetaData().getUpnpClass().startsWith("object.container")) {
                        coordinator.removeAllTracksFromQueue();
                        coordinator.addURIToQueue(theEntry);
                        coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");
                        String firstTrackNumberEnqueued = stateMap.get("FirstTrackNumberEnqueued");
                        if (firstTrackNumberEnqueued != null) {
                            coordinator.seek("TRACK_NR", firstTrackNumberEnqueued);
                        }
                    } else {
                        coordinator.setCurrentURI(theEntry);
                    }
                    coordinator.play();
                } catch (IllegalStateException e) {
                    logger.warn("Cannot paly favorite ({})", e.getMessage());
                }
            } else {
                logger.warn("Favorite '{}' not found", favorite);
            }

        }
    }

    public void playTrack(Command command) {

        if (command != null && command instanceof DecimalType) {
            try {
                ZonePlayerHandler coordinator = getCoordinatorHandler();

                String trackNumber = command.toString();

                coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");

                // seek the track - warning, we do not check if the tracknumber falls in the boundary of the queue
                coordinator.setPositionTrack(trackNumber);

                // take the system off mute
                coordinator.setMute(OnOffType.OFF);

                // start jammin'
                coordinator.play();
            } catch (IllegalStateException e) {
                logger.warn("Cannot play track ({})", e.getMessage());
            }
        }

    }

    public void playPlayList(Command command) {

        if (command != null && command instanceof StringType) {
            String playlist = command.toString();
            List<SonosEntry> playlists = getPlayLists();

            SonosEntry theEntry = null;
            // search for the appropriate play list based on its name (title)
            for (SonosEntry somePlaylist : playlists) {
                if (somePlaylist.getTitle().equals(playlist)) {
                    theEntry = somePlaylist;
                    break;
                }
            }

            // set the URI of the group coordinator
            if (theEntry != null) {
                try {
                    ZonePlayerHandler coordinator = getCoordinatorHandler();

                    coordinator.addURIToQueue(theEntry);

                    coordinator.setCurrentURI(QUEUE_URI + coordinator.getUDN() + "#0", "");

                    String firstTrackNumberEnqueued = stateMap.get("FirstTrackNumberEnqueued");
                    if (firstTrackNumberEnqueued != null) {
                        coordinator.seek("TRACK_NR", firstTrackNumberEnqueued);
                    }

                    coordinator.play();
                } catch (IllegalStateException e) {
                    logger.warn("Cannot play playlist ({})", e.getMessage());
                }
            } else {
                logger.warn("Playlist '{}' not found", playlist);
            }
        }
    }

    public void addURIToQueue(SonosEntry newEntry) {
        addURIToQueue(newEntry.getRes(), SonosXMLParser.compileMetadataString(newEntry), 1, true);
    }

    public String getZoneName() {
        return stateMap.get("ZoneName");
    }

    public String getZoneGroupID() {
        return stateMap.get("LocalGroupUUID");
    }

    public String getRunningAlarmProperties() {
        updateRunningAlarmProperties();
        return stateMap.get("RunningAlarmProperties");
    }

    public String getMute() {
        return stateMap.get("MuteMaster");
    }

    public boolean getLed() {
        return ((stateMap.get("CurrentLEDState") != null) && stateMap.get("CurrentLEDState").equals("On")) ? true
                : false;
    }

    public String getCurrentZoneName() {
        updateCurrentZoneName();
        return stateMap.get("CurrentZoneName");
    }

    @Override
    public void onStatusChanged(boolean status) {
        // TODO Auto-generated method stub

    }

    private String getModelNameFromDescriptor() {
        URL descriptor = service.getDescriptorURL(this);
        if (descriptor != null) {
            String sonosModelDescription = SonosXMLParser.parseModelDescription(service.getDescriptorURL(this));
            return SonosXMLParser.extractModelName(sonosModelDescription);
        } else {
            return null;
        }
    }

    private boolean migrateThingType() {
        if (getThing().getThingTypeUID().equals(ZONEPLAYER_THING_TYPE_UID)) {
            String modelName = getModelNameFromDescriptor();
            if (isSupportedModel(modelName)) {
                updateSonosThingType(modelName);
                return true;
            }
        }
        return false;
    }

    private boolean isSupportedModel(String modelName) {
        for (ThingTypeUID thingTypeUID : SUPPORTED_KNOWN_THING_TYPES_UIDS) {
            if (thingTypeUID.getId().equalsIgnoreCase(modelName)) {
                return true;
            }
        }
        return false;
    }

    private void updateSonosThingType(String newThingTypeID) {
        changeThingType(new ThingTypeUID(SonosBindingConstants.BINDING_ID, newThingTypeID), getConfig());
    }

    /*
     * Set the sleeptimer duration
     * Use String command of format "HH:MM:SS" to set the timer to the desired duration
     * Use empty String "" to switch the sleep timer off
     */
    public void setSleepTimer(Command command) {
        if (command != null) {
            if (command instanceof DecimalType) {

                Map<String, String> inputs = new HashMap<String, String>();
                inputs.put("InstanceID", "0");
                inputs.put("NewSleepTimerDuration", sleepSecondsToTimeStr(Integer.parseInt(command.toString())));

                this.service.invokeAction(this, "AVTransport", "ConfigureSleepTimer", inputs);
            }
        }
    }

    protected void updateSleepTimerDuration() {
        Map<String, String> result = service.invokeAction(this, "AVTransport", "GetRemainingSleepTimerDuration",
                null);
        for (String variable : result.keySet()) {
            this.onValueReceived(variable, result.get(variable), "AVTransport");
        }
    }

    private String sleepSecondsToTimeStr(long sleepSeconds) {
        if (sleepSeconds == 0) {
            return "";
        } else if (sleepSeconds < 68400) {
            long hours = TimeUnit.SECONDS.toHours(sleepSeconds);
            sleepSeconds -= TimeUnit.HOURS.toSeconds(hours);
            long minutes = TimeUnit.SECONDS.toMinutes(sleepSeconds);
            sleepSeconds -= TimeUnit.MINUTES.toSeconds(minutes);
            long seconds = TimeUnit.SECONDS.toSeconds(sleepSeconds);
            return String.format("%02d:%02d:%02d", hours, minutes, seconds);
        } else {
            logger.error("Sonos SleepTimer: Invalid sleep time set. sleep time must be >=0 and < 68400s (24h)");
            return "ERR";
        }

    }

    private long sleepStrTimeToSeconds(String sleepTime) {
        String[] units = sleepTime.split(":");
        int hours = Integer.parseInt(units[0]);
        int minutes = Integer.parseInt(units[1]);
        int seconds = Integer.parseInt(units[2]);
        return 3600 * hours + 60 * minutes + seconds;
    }
}