org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.binding.squeezebox.internal.handler.SqueezeBoxPlayerHandler.java

Source

/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.binding.squeezebox.internal.handler;

import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.smarthome.core.cache.ExpiringCacheMap;
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.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.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.ThingStatusInfo;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.StateOption;
import org.eclipse.smarthome.core.types.UnDefType;
import org.eclipse.smarthome.io.net.http.HttpUtil;
import org.openhab.binding.squeezebox.internal.SqueezeBoxStateDescriptionOptionsProvider;
import org.openhab.binding.squeezebox.internal.config.SqueezeBoxPlayerConfig;
import org.openhab.binding.squeezebox.internal.model.Favorite;
import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The {@link SqueezeBoxPlayerHandler} is responsible for handling states, which
 * are sent to/from channels.
 *
 * @author Dan Cunningham - Initial contribution
 * @author Mark Hilbush - Improved handling of player status, prevent REFRESH from causing exception
 * @author Mark Hilbush - Implement AudioSink and notifications
 * @author Mark Hilbush - Added duration channel
 * @author Patrik Gfeller - Timeout for TTS messages increased from 30 to 90s.
 * @author Mark Hilbush - Get favorites from server and play favorite
 * @author Mark Hilbush - Convert sound notification volume from channel to config parameter
 */
public class SqueezeBoxPlayerHandler extends BaseThingHandler implements SqueezeBoxPlayerEventListener {
    private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerHandler.class);

    public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
            .singleton(SQUEEZEBOXPLAYER_THING_TYPE);

    /**
     * We need to remember some states to change offsets in volume, time index,
     * etc..
     */
    protected Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<String, State>());

    /**
     * Keeps current track time
     */
    ScheduledFuture<?> timeCounterJob;

    /**
     * Local reference to our bridge
     */
    private SqueezeBoxServerHandler squeezeBoxServerHandler;

    /**
     * Our mac address, needed everywhere
     */
    private String mac;

    /**
     * Before we mute or recieve a mute event, store our current volume
     */
    private int unmuteVolume = 0;

    /**
     * The server sends us the current time on play/pause/stop events, we
     * increment it locally from there on
     */
    private int currentTime = 0;

    /**
     * Our we playing something right now or not, need to keep current track
     * time
     */
    private boolean playing;

    /**
     * Separate volume level for notifications
     */
    private Integer notificationSoundVolume = null;

    private String callbackUrl;

    private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;

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

    /**
     * Creates SqueezeBox Player Handler
     *
     * @param thing
     * @param stateDescriptionProvider
     */
    public SqueezeBoxPlayerHandler(@NonNull Thing thing, String callbackUrl,
            SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) {
        super(thing);
        this.callbackUrl = callbackUrl;
        this.stateDescriptionProvider = stateDescriptionProvider;
    }

    @Override
    public void initialize() {
        mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac;
        timeCounter();
        updateBridgeStatus();
        logger.debug("player thing {} initialized.", getThing().getUID());
    }

    @Override
    public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
        updateBridgeStatus();
    }

    private void updateBridgeStatus() {
        ThingStatus bridgeStatus = getBridge().getStatus();
        if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) {
            updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
            squeezeBoxServerHandler = (SqueezeBoxServerHandler) getBridge().getHandler();
        } else if (bridgeStatus == ThingStatus.OFFLINE) {
            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
        }
    }

    @Override
    public void dispose() {
        // stop our duration counter
        if (timeCounterJob != null && !timeCounterJob.isCancelled()) {
            timeCounterJob.cancel(true);
            timeCounterJob = null;
        }

        if (squeezeBoxServerHandler != null) {
            squeezeBoxServerHandler.removePlayerCache(mac);
        }
        logger.debug("player thing {} disposed.", getThing().getUID());
        super.dispose();
    }

    @Override
    public void handleCommand(ChannelUID channelUID, Command command) {
        if (squeezeBoxServerHandler == null) {
            logger.info("player thing {} has no server configured, ignoring command: {}", getThing().getUID(),
                    command);
            return;
        }
        String mac = getConfigAs(SqueezeBoxPlayerConfig.class).mac;

        // Some of the code below is not designed to handle REFRESH
        if (command == RefreshType.REFRESH) {
            return;
        }

        switch (channelUID.getIdWithoutGroup()) {
        case CHANNEL_POWER:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.powerOn(mac);
            } else {
                squeezeBoxServerHandler.powerOff(mac);
            }
            break;
        case CHANNEL_MUTE:
            if (command.equals(OnOffType.ON)) {
                mute();
            } else {
                squeezeBoxServerHandler.unMute(mac, unmuteVolume);
            }
            break;
        case CHANNEL_STOP:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.stop(mac);
            } else if (command.equals(OnOffType.OFF)) {
                squeezeBoxServerHandler.play(mac);
            }
            break;
        case CHANNEL_PLAY_PAUSE:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.play(mac);
            } else if (command.equals(OnOffType.OFF)) {
                squeezeBoxServerHandler.pause(mac);
            }
            break;
        case CHANNEL_PREV:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.prev(mac);
            }
            break;
        case CHANNEL_NEXT:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.next(mac);
            }
            break;
        case CHANNEL_VOLUME:
            if (command instanceof PercentType) {
                squeezeBoxServerHandler.setVolume(mac, ((PercentType) command).intValue());
            } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
                squeezeBoxServerHandler.volumeUp(mac, currentVolume());
            } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
                squeezeBoxServerHandler.volumeDown(mac, currentVolume());
            } else if (command.equals(OnOffType.OFF)) {
                mute();
            } else if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.unMute(mac, unmuteVolume);
            }
            break;
        case CHANNEL_CONTROL:
            if (command instanceof PlayPauseType) {
                if (command.equals(PlayPauseType.PLAY)) {
                    squeezeBoxServerHandler.play(mac);
                } else if (command.equals(PlayPauseType.PAUSE)) {
                    squeezeBoxServerHandler.pause(mac);
                }
            }
            if (command instanceof NextPreviousType) {
                if (command.equals(NextPreviousType.NEXT)) {
                    squeezeBoxServerHandler.next(mac);
                } else if (command.equals(NextPreviousType.PREVIOUS)) {
                    squeezeBoxServerHandler.prev(mac);
                }
            }
            if (command instanceof RewindFastforwardType) {
                if (command.equals(RewindFastforwardType.REWIND)) {
                    squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() - 5);
                } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
                    squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() + 5);
                }
            }
            break;
        case CHANNEL_STREAM:
            squeezeBoxServerHandler.playUrl(mac, command.toString());
            break;
        case CHANNEL_SYNC:
            if (StringUtils.isBlank(command.toString())) {
                squeezeBoxServerHandler.unSyncPlayer(mac);
            } else {
                squeezeBoxServerHandler.syncPlayer(mac, command.toString());
            }
            break;
        case CHANNEL_UNSYNC:
            if (command.equals(OnOffType.ON)) {
                squeezeBoxServerHandler.unSyncPlayer(mac);
            }
            break;
        case CHANNEL_PLAYLIST_INDEX:
            squeezeBoxServerHandler.playPlaylistItem(mac, ((DecimalType) command).intValue());
            break;
        case CHANNEL_CURRENT_PLAYING_TIME:
            squeezeBoxServerHandler.setPlayingTime(mac, ((DecimalType) command).intValue());
            break;
        case CHANNEL_CURRENT_PLAYLIST_SHUFFLE:
            squeezeBoxServerHandler.setShuffleMode(mac, ((DecimalType) command).intValue());
            break;
        case CHANNEL_CURRENT_PLAYLIST_REPEAT:
            squeezeBoxServerHandler.setRepeatMode(mac, ((DecimalType) command).intValue());
            break;
        case CHANNEL_FAVORITES_PLAY:
            squeezeBoxServerHandler.playFavorite(mac, command.toString());
            break;
        default:
            break;
        }
    }

    @Override
    public void playerAdded(SqueezeBoxPlayer player) {
        // Player properties are saved in SqueezeBoxPlayerDiscoveryParticipant
    }

    @Override
    public void powerChangeEvent(String mac, boolean power) {
        updateChannel(mac, CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
        if (!power && isMe(mac)) {
            playing = false;
        }
    }

    @Override
    public synchronized void modeChangeEvent(String mac, String mode) {
        updateChannel(mac, CHANNEL_CONTROL, "play".equals(mode) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
        updateChannel(mac, CHANNEL_PLAY_PAUSE, "play".equals(mode) ? OnOffType.ON : OnOffType.OFF);
        updateChannel(mac, CHANNEL_STOP, "stop".equals(mode) ? OnOffType.ON : OnOffType.OFF);
        if (isMe(mac)) {
            playing = "play".equalsIgnoreCase(mode);
        }
    }

    @Override
    public void absoluteVolumeChangeEvent(String mac, int volume) {
        int newVolume = volume;
        newVolume = Math.min(100, newVolume);
        newVolume = Math.max(0, newVolume);
        updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
    }

    @Override
    public void relativeVolumeChangeEvent(String mac, int volumeChange) {
        int newVolume = currentVolume() + volumeChange;
        newVolume = Math.min(100, newVolume);
        newVolume = Math.max(0, newVolume);
        updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));

        if (isMe(mac)) {
            logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, newVolume);
        }
    }

    @Override
    public void muteChangeEvent(String mac, boolean mute) {
        unmuteVolume = currentVolume();
        updateChannel(mac, CHANNEL_MUTE, mute ? OnOffType.ON : OnOffType.OFF);
    }

    @Override
    public void currentPlaylistIndexEvent(String mac, int index) {
        updateChannel(mac, CHANNEL_PLAYLIST_INDEX, new DecimalType(index));
    }

    @Override
    public void currentPlayingTimeEvent(String mac, int time) {
        updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(time));
        if (isMe(mac)) {
            currentTime = time;
        }
    }

    @Override
    public void durationEvent(String mac, int duration) {
        if (getThing().getChannel(CHANNEL_DURATION) == null) {
            logger.debug("Channel 'duration' does not exist.  Delete and readd player thing to pick up channel.");
            return;
        }
        updateChannel(mac, CHANNEL_DURATION, new DecimalType(duration));
    }

    @Override
    public void numberPlaylistTracksEvent(String mac, int track) {
        updateChannel(mac, CHANNEL_NUMBER_PLAYLIST_TRACKS, new DecimalType(track));
    }

    @Override
    public void currentPlaylistShuffleEvent(String mac, int shuffle) {
        updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_SHUFFLE, new DecimalType(shuffle));

    }

    @Override
    public void currentPlaylistRepeatEvent(String mac, int repeat) {
        updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_REPEAT, new DecimalType(repeat));
    }

    @Override
    public void titleChangeEvent(String mac, String title) {
        updateChannel(mac, CHANNEL_TITLE, new StringType(title));

    }

    @Override
    public void albumChangeEvent(String mac, String album) {
        updateChannel(mac, CHANNEL_ALBUM, new StringType(album));
    }

    @Override
    public void artistChangeEvent(String mac, String artist) {
        updateChannel(mac, CHANNEL_ARTIST, new StringType(artist));
    }

    @Override
    public void coverArtChangeEvent(String mac, String coverArtUrl) {
        updateChannel(mac, CHANNEL_COVERART_DATA, createImage(downloadImage(mac, coverArtUrl)));
    }

    /**
     * Download and cache the image data from an URL.
     *
     * @param url The URL of the image to be downloaded.
     * @return A RawType object containing the image, null if the content type could not be found or the content type is
     *         not an image.
     */
    private RawType downloadImage(String mac, String url) {
        // Only get the image if this is my PlayerHandler instance
        if (isMe(mac)) {
            if (StringUtils.isNotEmpty(url)) {
                String sanitizedUrl = sanitizeUrl(url);
                RawType image = IMAGE_CACHE.putIfAbsentAndGet(url, () -> {
                    logger.debug("Trying to download the content of URL {}", sanitizedUrl);
                    try {
                        return HttpUtil.downloadImage(url);
                    } catch (IllegalArgumentException e) {
                        logger.debug("IllegalArgumentException when downloading image from {}", sanitizedUrl, e);
                        return null;
                    }
                });
                if (image == null) {
                    logger.debug("Failed to download the content of URL {}", sanitizedUrl);
                    return null;
                } else {
                    return image;
                }
            }
        }
        return null;
    }

    /*
     * Replaces the password in the URL, if present
     */
    private String sanitizeUrl(String url) {
        String sanitizedUrl = url;
        try {
            URI uri = new URI(url);
            String userInfo = uri.getUserInfo();
            if (userInfo != null) {
                String[] userInfoParts = userInfo.split(":");
                if (userInfoParts.length == 2) {
                    sanitizedUrl = url.replace(userInfoParts[1], "**********");
                }
            }
        } catch (URISyntaxException e) {
            // Just return what was passed in
        }
        return sanitizedUrl;
    }

    /**
     * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
     */
    private State createImage(RawType image) {
        if (image == null) {
            return UnDefType.UNDEF;
        } else {
            return image;
        }
    }

    @Override
    public void yearChangeEvent(String mac, String year) {
        updateChannel(mac, CHANNEL_YEAR, new StringType(year));
    }

    @Override
    public void genreChangeEvent(String mac, String genre) {
        updateChannel(mac, CHANNEL_GENRE, new StringType(genre));
    }

    @Override
    public void remoteTitleChangeEvent(String mac, String title) {
        updateChannel(mac, CHANNEL_REMOTE_TITLE, new StringType(title));
    }

    @Override
    public void irCodeChangeEvent(String mac, String ircode) {
        if (isMe(mac)) {
            postCommand(CHANNEL_IRCODE, new StringType(ircode));
        }
    }

    @Override
    public void updateFavoritesListEvent(List<Favorite> favorites) {
        logger.debug("Player {} updating favorites list", mac);
        List<StateOption> options = new ArrayList<>();
        for (Favorite favorite : favorites) {
            options.add(new StateOption(favorite.shortId, favorite.name));
        }
        stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FAVORITES_PLAY),
                options);
    }

    /**
     * Update a channel if the mac matches our own
     *
     * @param mac
     * @param channelID
     * @param state
     */
    private void updateChannel(String mac, String channelID, State state) {
        if (isMe(mac)) {
            State prevState = stateMap.put(channelID, state);
            if (prevState == null || !prevState.equals(state)) {
                logger.trace("Updating channel {} for thing {} with mac {} to state {}", channelID,
                        getThing().getUID(), mac, state);
                updateState(channelID, state);
            }
        }
    }

    /**
     * Helper method to mute a player
     */
    private void mute() {
        unmuteVolume = currentVolume();
        squeezeBoxServerHandler.mute(mac);
    }

    /**
     * Helper methods to get the current state of the player
     *
     * @return
     */
    int currentVolume() {
        if (stateMap.containsKey(CHANNEL_VOLUME)) {
            return ((DecimalType) stateMap.get(CHANNEL_VOLUME)).intValue();
        } else {
            return 0;
        }
    }

    int currentPlayingTime() {
        if (stateMap.containsKey(CHANNEL_CURRENT_PLAYING_TIME)) {
            return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYING_TIME)).intValue();
        } else {
            return 0;
        }
    }

    int currentNumberPlaylistTracks() {
        if (stateMap.containsKey(CHANNEL_NUMBER_PLAYLIST_TRACKS)) {
            return ((DecimalType) stateMap.get(CHANNEL_NUMBER_PLAYLIST_TRACKS)).intValue();
        } else {
            return 0;
        }
    }

    int currentPlaylistIndex() {
        if (stateMap.containsKey(CHANNEL_PLAYLIST_INDEX)) {
            return ((DecimalType) stateMap.get(CHANNEL_PLAYLIST_INDEX)).intValue();
        } else {
            return 0;
        }
    }

    boolean currentPower() {
        if (stateMap.containsKey(CHANNEL_POWER)) {
            return (stateMap.get(CHANNEL_POWER).equals(OnOffType.ON) ? true : false);
        } else {
            return false;
        }
    }

    boolean currentStop() {
        if (stateMap.containsKey(CHANNEL_STOP)) {
            return (stateMap.get(CHANNEL_STOP).equals(OnOffType.ON) ? true : false);
        } else {
            return false;
        }
    }

    boolean currentControl() {
        if (stateMap.containsKey(CHANNEL_CONTROL)) {
            return (stateMap.get(CHANNEL_CONTROL).equals(PlayPauseType.PLAY) ? true : false);
        } else {
            return false;
        }
    }

    boolean currentMute() {
        if (stateMap.containsKey(CHANNEL_MUTE)) {
            return (stateMap.get(CHANNEL_MUTE).equals(OnOffType.ON) ? true : false);
        } else {
            return false;
        }
    }

    int currentShuffle() {
        if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)) {
            return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)).intValue();
        } else {
            return 0;
        }
    }

    int currentRepeat() {
        if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_REPEAT)) {
            return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_REPEAT)).intValue();
        } else {
            return 0;
        }
    }

    /**
     * Ticks away when in a play state to keep current track time
     */
    private void timeCounter() {
        timeCounterJob = scheduler.scheduleWithFixedDelay(() -> {
            if (playing) {
                updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(currentTime++));
            }
        }, 0, 1, TimeUnit.SECONDS);
    }

    private boolean isMe(String mac) {
        return mac.equals(this.mac);
    }

    /**
     * Returns our server handler if set
     *
     * @return
     */
    public SqueezeBoxServerHandler getSqueezeBoxServerHandler() {
        return this.squeezeBoxServerHandler;
    }

    /**
     * Returns the MAC address for this player
     *
     * @return
     */
    public String getMac() {
        return this.mac;
    }

    /*
     * Give the notification player access to the notification timeout
     */
    public int getNotificationTimeout() {
        return getConfigAs(SqueezeBoxPlayerConfig.class).notificationTimeout;
    }

    /*
     * Used by the AudioSink to get the volume level that should be used for the notification.
     * Priority for determining volume is:
     * - volume is provided in the say/playSound actions
     * - volume is contained in the player thing's configuration
     * - current player volume setting
     */
    public PercentType getNotificationSoundVolume() {
        // Get the notification sound volume from this player thing's configuration
        Integer configNotificationSoundVolume = getConfigAs(SqueezeBoxPlayerConfig.class).notificationVolume;

        // Determine which volume to use
        Integer currentNotificationSoundVolume;
        if (notificationSoundVolume != null) {
            currentNotificationSoundVolume = notificationSoundVolume;
        } else if (configNotificationSoundVolume != null) {
            currentNotificationSoundVolume = configNotificationSoundVolume;
        } else {
            currentNotificationSoundVolume = Integer.valueOf(currentVolume());
        }
        return new PercentType(currentNotificationSoundVolume.intValue());
    }

    /*
     * Used by the AudioSink to set the volume level that should be used to play the notification
     */
    public void setNotificationSoundVolume(PercentType newNotificationSoundVolume) {
        if (newNotificationSoundVolume != null) {
            notificationSoundVolume = Integer.valueOf(newNotificationSoundVolume.intValue());
        }
    }

    /*
     * Play the notification.
     */
    public void playNotificationSoundURI(StringType uri) {
        logger.debug("Play notification sound on player {} at URI {}", mac, uri);

        try (SqueezeBoxNotificationPlayer notificationPlayer = new SqueezeBoxNotificationPlayer(this,
                squeezeBoxServerHandler, uri)) {
            notificationPlayer.play();
        } catch (InterruptedException e) {
            logger.warn("Notification playback was interrupted", e);
        } catch (SqueezeBoxTimeoutException e) {
            logger.debug("SqueezeBoxTimeoutException during notification: {}", e.getMessage());
        } finally {
            notificationSoundVolume = null;
        }
    }

    /*
     * Return the IP and port of the OH2 web server
     */
    public String getHostAndPort() {
        return callbackUrl;
    }
}