]> git.basschouten.com Git - openhab-addons.git/blob
aa02f8e479e9303538ac455f664bca6a21f0ff70
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.squeezebox.internal.handler;
14
15 import static org.openhab.binding.squeezebox.internal.SqueezeBoxBindingConstants.*;
16
17 import java.net.URI;
18 import java.net.URISyntaxException;
19 import java.time.Duration;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.HashMap;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.openhab.binding.squeezebox.internal.SqueezeBoxStateDescriptionOptionsProvider;
32 import org.openhab.binding.squeezebox.internal.config.SqueezeBoxPlayerConfig;
33 import org.openhab.binding.squeezebox.internal.model.Favorite;
34 import org.openhab.binding.squeezebox.internal.utils.SqueezeBoxTimeoutException;
35 import org.openhab.core.cache.ExpiringCacheMap;
36 import org.openhab.core.io.net.http.HttpUtil;
37 import org.openhab.core.library.types.DecimalType;
38 import org.openhab.core.library.types.IncreaseDecreaseType;
39 import org.openhab.core.library.types.NextPreviousType;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.library.types.PercentType;
42 import org.openhab.core.library.types.PlayPauseType;
43 import org.openhab.core.library.types.RawType;
44 import org.openhab.core.library.types.RewindFastforwardType;
45 import org.openhab.core.library.types.StringType;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.ThingStatusInfo;
51 import org.openhab.core.thing.ThingTypeUID;
52 import org.openhab.core.thing.binding.BaseThingHandler;
53 import org.openhab.core.types.Command;
54 import org.openhab.core.types.RefreshType;
55 import org.openhab.core.types.State;
56 import org.openhab.core.types.StateOption;
57 import org.openhab.core.types.UnDefType;
58 import org.slf4j.Logger;
59 import org.slf4j.LoggerFactory;
60
61 /**
62  * The {@link SqueezeBoxPlayerHandler} is responsible for handling states, which
63  * are sent to/from channels.
64  *
65  * @author Dan Cunningham - Initial contribution
66  * @author Mark Hilbush - Improved handling of player status, prevent REFRESH from causing exception
67  * @author Mark Hilbush - Implement AudioSink and notifications
68  * @author Mark Hilbush - Added duration channel
69  * @author Patrik Gfeller - Timeout for TTS messages increased from 30 to 90s.
70  * @author Mark Hilbush - Get favorites from server and play favorite
71  * @author Mark Hilbush - Convert sound notification volume from channel to config parameter
72  * @author Mark Hilbush - Add like/unlike functionality
73  */
74 public class SqueezeBoxPlayerHandler extends BaseThingHandler implements SqueezeBoxPlayerEventListener {
75     private final Logger logger = LoggerFactory.getLogger(SqueezeBoxPlayerHandler.class);
76
77     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
78             .singleton(SQUEEZEBOXPLAYER_THING_TYPE);
79
80     /**
81      * We need to remember some states to change offsets in volume, time index,
82      * etc..
83      */
84     protected Map<String, State> stateMap = Collections.synchronizedMap(new HashMap<>());
85
86     /**
87      * Keeps current track time
88      */
89     private ScheduledFuture<?> timeCounterJob;
90
91     /**
92      * Local reference to our bridge
93      */
94     private SqueezeBoxServerHandler squeezeBoxServerHandler;
95
96     /**
97      * Our mac address, needed everywhere
98      */
99     private String mac;
100
101     /**
102      * The server sends us the current time on play/pause/stop events, we
103      * increment it locally from there on
104      */
105     private int currentTime = 0;
106
107     /**
108      * Our we playing something right now or not, need to keep current track
109      * time
110      */
111     private boolean playing;
112
113     /**
114      * Separate volume level for notifications
115      */
116     private Integer notificationSoundVolume = null;
117
118     private String callbackUrl;
119
120     private SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider;
121
122     private static final ExpiringCacheMap<String, RawType> IMAGE_CACHE = new ExpiringCacheMap<>(
123             TimeUnit.MINUTES.toMillis(15)); // 15min
124
125     private String likeCommand;
126     private String unlikeCommand;
127
128     /**
129      * Creates SqueezeBox Player Handler
130      *
131      * @param thing
132      * @param stateDescriptionProvider
133      */
134     public SqueezeBoxPlayerHandler(@NonNull Thing thing, String callbackUrl,
135             SqueezeBoxStateDescriptionOptionsProvider stateDescriptionProvider) {
136         super(thing);
137         this.callbackUrl = callbackUrl;
138         this.stateDescriptionProvider = stateDescriptionProvider;
139     }
140
141     @Override
142     public void initialize() {
143         mac = getConfig().as(SqueezeBoxPlayerConfig.class).mac;
144         timeCounter();
145         updateBridgeStatus();
146         logger.debug("player thing {} initialized with mac {}", getThing().getUID(), mac);
147     }
148
149     @Override
150     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
151         updateBridgeStatus();
152     }
153
154     private void updateBridgeStatus() {
155         Thing bridge = getBridge();
156         if (bridge != null) {
157             squeezeBoxServerHandler = (SqueezeBoxServerHandler) bridge.getHandler();
158             ThingStatus bridgeStatus = bridge.getStatus();
159             if (bridgeStatus == ThingStatus.ONLINE && getThing().getStatus() != ThingStatus.ONLINE) {
160                 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
161             } else if (bridgeStatus == ThingStatus.OFFLINE) {
162                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
163             }
164         } else {
165             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge not found");
166         }
167     }
168
169     @Override
170     public void dispose() {
171         // stop our duration counter
172         if (timeCounterJob != null && !timeCounterJob.isCancelled()) {
173             timeCounterJob.cancel(true);
174             timeCounterJob = null;
175         }
176
177         if (squeezeBoxServerHandler != null) {
178             squeezeBoxServerHandler.removePlayerCache(mac);
179         }
180         logger.debug("player thing {} disposed for mac {}", getThing().getUID(), mac);
181         super.dispose();
182     }
183
184     @Override
185     public void handleCommand(ChannelUID channelUID, Command command) {
186         if (squeezeBoxServerHandler == null) {
187             logger.debug("Player {} has no server configured, ignoring command: {}", getThing().getUID(), command);
188             return;
189         }
190         // Some of the code below is not designed to handle REFRESH, only reply to channels where cached values exist
191         if (command == RefreshType.REFRESH) {
192             String channelID = channelUID.getId();
193             State newState = stateMap.get(channelID);
194             if (newState != null) {
195                 updateState(channelID, newState);
196             }
197             return;
198         }
199
200         switch (channelUID.getIdWithoutGroup()) {
201             case CHANNEL_POWER:
202                 if (command.equals(OnOffType.ON)) {
203                     squeezeBoxServerHandler.powerOn(mac);
204                 } else {
205                     squeezeBoxServerHandler.powerOff(mac);
206                 }
207                 break;
208             case CHANNEL_MUTE:
209                 if (command.equals(OnOffType.ON)) {
210                     squeezeBoxServerHandler.mute(mac);
211                 } else {
212                     squeezeBoxServerHandler.unMute(mac);
213                 }
214                 break;
215             case CHANNEL_STOP:
216                 if (command.equals(OnOffType.ON)) {
217                     squeezeBoxServerHandler.stop(mac);
218                 } else if (command.equals(OnOffType.OFF)) {
219                     squeezeBoxServerHandler.play(mac);
220                 }
221                 break;
222             case CHANNEL_PLAY_PAUSE:
223                 if (command.equals(OnOffType.ON)) {
224                     squeezeBoxServerHandler.play(mac);
225                 } else if (command.equals(OnOffType.OFF)) {
226                     squeezeBoxServerHandler.pause(mac);
227                 }
228                 break;
229             case CHANNEL_PREV:
230                 if (command.equals(OnOffType.ON)) {
231                     squeezeBoxServerHandler.prev(mac);
232                 }
233                 break;
234             case CHANNEL_NEXT:
235                 if (command.equals(OnOffType.ON)) {
236                     squeezeBoxServerHandler.next(mac);
237                 }
238                 break;
239             case CHANNEL_VOLUME:
240                 if (command instanceof PercentType) {
241                     squeezeBoxServerHandler.setVolume(mac, ((PercentType) command).intValue());
242                 } else if (command.equals(IncreaseDecreaseType.INCREASE)) {
243                     squeezeBoxServerHandler.volumeUp(mac, currentVolume());
244                 } else if (command.equals(IncreaseDecreaseType.DECREASE)) {
245                     squeezeBoxServerHandler.volumeDown(mac, currentVolume());
246                 } else if (command.equals(OnOffType.OFF)) {
247                     squeezeBoxServerHandler.mute(mac);
248                 } else if (command.equals(OnOffType.ON)) {
249                     squeezeBoxServerHandler.unMute(mac);
250                 }
251                 break;
252             case CHANNEL_CONTROL:
253                 if (command instanceof PlayPauseType) {
254                     if (command.equals(PlayPauseType.PLAY)) {
255                         squeezeBoxServerHandler.play(mac);
256                     } else if (command.equals(PlayPauseType.PAUSE)) {
257                         squeezeBoxServerHandler.pause(mac);
258                     }
259                 }
260                 if (command instanceof NextPreviousType) {
261                     if (command.equals(NextPreviousType.NEXT)) {
262                         squeezeBoxServerHandler.next(mac);
263                     } else if (command.equals(NextPreviousType.PREVIOUS)) {
264                         squeezeBoxServerHandler.prev(mac);
265                     }
266                 }
267                 if (command instanceof RewindFastforwardType) {
268                     if (command.equals(RewindFastforwardType.REWIND)) {
269                         squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() - 5);
270                     } else if (command.equals(RewindFastforwardType.FASTFORWARD)) {
271                         squeezeBoxServerHandler.setPlayingTime(mac, currentPlayingTime() + 5);
272                     }
273                 }
274                 break;
275             case CHANNEL_STREAM:
276                 squeezeBoxServerHandler.playUrl(mac, command.toString());
277                 break;
278             case CHANNEL_SYNC:
279                 if (StringUtils.isBlank(command.toString())) {
280                     squeezeBoxServerHandler.unSyncPlayer(mac);
281                 } else {
282                     squeezeBoxServerHandler.syncPlayer(mac, command.toString());
283                 }
284                 break;
285             case CHANNEL_UNSYNC:
286                 if (command.equals(OnOffType.ON)) {
287                     squeezeBoxServerHandler.unSyncPlayer(mac);
288                 }
289                 break;
290             case CHANNEL_PLAYLIST_INDEX:
291                 squeezeBoxServerHandler.playPlaylistItem(mac, ((DecimalType) command).intValue());
292                 break;
293             case CHANNEL_CURRENT_PLAYING_TIME:
294                 squeezeBoxServerHandler.setPlayingTime(mac, ((DecimalType) command).intValue());
295                 break;
296             case CHANNEL_CURRENT_PLAYLIST_SHUFFLE:
297                 squeezeBoxServerHandler.setShuffleMode(mac, ((DecimalType) command).intValue());
298                 break;
299             case CHANNEL_CURRENT_PLAYLIST_REPEAT:
300                 squeezeBoxServerHandler.setRepeatMode(mac, ((DecimalType) command).intValue());
301                 break;
302             case CHANNEL_FAVORITES_PLAY:
303                 squeezeBoxServerHandler.playFavorite(mac, command.toString());
304                 break;
305             case CHANNEL_RATE:
306                 if (command.equals(OnOffType.ON)) {
307                     squeezeBoxServerHandler.rate(mac, likeCommand);
308                 } else if (command.equals(OnOffType.OFF)) {
309                     squeezeBoxServerHandler.rate(mac, unlikeCommand);
310                 }
311                 break;
312             case CHANNEL_SLEEP:
313                 if (command instanceof DecimalType) {
314                     Duration sleepDuration = Duration.ofMinutes(((DecimalType) command).longValue());
315                     if (sleepDuration.isNegative() || sleepDuration.compareTo(Duration.ofDays(1)) > 0) {
316                         logger.debug("Sleep timer of {} minutes must be >= 0 and <= 1 day", sleepDuration.toMinutes());
317                         return;
318                     }
319                     squeezeBoxServerHandler.sleep(mac, sleepDuration);
320                 }
321                 break;
322             default:
323                 break;
324         }
325     }
326
327     @Override
328     public void playerAdded(SqueezeBoxPlayer player) {
329         // Player properties are saved in SqueezeBoxPlayerDiscoveryParticipant
330     }
331
332     @Override
333     public void powerChangeEvent(String mac, boolean power) {
334         updateChannel(mac, CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
335         if (!power && isMe(mac)) {
336             playing = false;
337         }
338     }
339
340     @Override
341     public synchronized void modeChangeEvent(String mac, String mode) {
342         updateChannel(mac, CHANNEL_CONTROL, "play".equals(mode) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
343         updateChannel(mac, CHANNEL_PLAY_PAUSE, "play".equals(mode) ? OnOffType.ON : OnOffType.OFF);
344         updateChannel(mac, CHANNEL_STOP, "stop".equals(mode) ? OnOffType.ON : OnOffType.OFF);
345         if (isMe(mac)) {
346             playing = "play".equalsIgnoreCase(mode);
347         }
348     }
349
350     @Override
351     public void sourceChangeEvent(String mac, String source) {
352         updateChannel(mac, CHANNEL_SOURCE, StringType.valueOf(source));
353     }
354
355     @Override
356     public void absoluteVolumeChangeEvent(String mac, int volume) {
357         int newVolume = volume;
358         newVolume = Math.min(100, newVolume);
359         newVolume = Math.max(0, newVolume);
360         updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
361     }
362
363     @Override
364     public void relativeVolumeChangeEvent(String mac, int volumeChange) {
365         int newVolume = currentVolume() + volumeChange;
366         newVolume = Math.min(100, newVolume);
367         newVolume = Math.max(0, newVolume);
368         updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
369
370         if (isMe(mac)) {
371             logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, newVolume);
372         }
373     }
374
375     @Override
376     public void muteChangeEvent(String mac, boolean mute) {
377         updateChannel(mac, CHANNEL_MUTE, mute ? OnOffType.ON : OnOffType.OFF);
378     }
379
380     @Override
381     public void currentPlaylistIndexEvent(String mac, int index) {
382         updateChannel(mac, CHANNEL_PLAYLIST_INDEX, new DecimalType(index));
383     }
384
385     @Override
386     public void currentPlayingTimeEvent(String mac, int time) {
387         updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(time));
388         if (isMe(mac)) {
389             currentTime = time;
390         }
391     }
392
393     @Override
394     public void durationEvent(String mac, int duration) {
395         if (getThing().getChannel(CHANNEL_DURATION) == null) {
396             logger.debug("Channel 'duration' does not exist.  Delete and readd player thing to pick up channel.");
397             return;
398         }
399         updateChannel(mac, CHANNEL_DURATION, new DecimalType(duration));
400     }
401
402     @Override
403     public void numberPlaylistTracksEvent(String mac, int track) {
404         updateChannel(mac, CHANNEL_NUMBER_PLAYLIST_TRACKS, new DecimalType(track));
405     }
406
407     @Override
408     public void currentPlaylistShuffleEvent(String mac, int shuffle) {
409         updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_SHUFFLE, new DecimalType(shuffle));
410     }
411
412     @Override
413     public void currentPlaylistRepeatEvent(String mac, int repeat) {
414         updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_REPEAT, new DecimalType(repeat));
415     }
416
417     @Override
418     public void titleChangeEvent(String mac, String title) {
419         updateChannel(mac, CHANNEL_TITLE, new StringType(title));
420     }
421
422     @Override
423     public void albumChangeEvent(String mac, String album) {
424         updateChannel(mac, CHANNEL_ALBUM, new StringType(album));
425     }
426
427     @Override
428     public void artistChangeEvent(String mac, String artist) {
429         updateChannel(mac, CHANNEL_ARTIST, new StringType(artist));
430     }
431
432     @Override
433     public void coverArtChangeEvent(String mac, String coverArtUrl) {
434         updateChannel(mac, CHANNEL_COVERART_DATA, createImage(downloadImage(mac, coverArtUrl)));
435     }
436
437     /**
438      * Download and cache the image data from an URL.
439      *
440      * @param url The URL of the image to be downloaded.
441      * @return A RawType object containing the image, null if the content type could not be found or the content type is
442      *         not an image.
443      */
444     private RawType downloadImage(String mac, String url) {
445         // Only get the image if this is my PlayerHandler instance
446         if (isMe(mac)) {
447             if (StringUtils.isNotEmpty(url)) {
448                 String sanitizedUrl = sanitizeUrl(url);
449                 RawType image = IMAGE_CACHE.putIfAbsentAndGet(url, () -> {
450                     logger.debug("Trying to download the content of URL {}", sanitizedUrl);
451                     try {
452                         return HttpUtil.downloadImage(url);
453                     } catch (IllegalArgumentException e) {
454                         logger.debug("IllegalArgumentException when downloading image from {}", sanitizedUrl, e);
455                         return null;
456                     }
457                 });
458                 if (image == null) {
459                     logger.debug("Failed to download the content of URL {}", sanitizedUrl);
460                     return null;
461                 } else {
462                     return image;
463                 }
464             }
465         }
466         return null;
467     }
468
469     /*
470      * Replaces the password in the URL, if present
471      */
472     private String sanitizeUrl(String url) {
473         String sanitizedUrl = url;
474         try {
475             URI uri = new URI(url);
476             String userInfo = uri.getUserInfo();
477             if (userInfo != null) {
478                 String[] userInfoParts = userInfo.split(":");
479                 if (userInfoParts.length == 2) {
480                     sanitizedUrl = url.replace(userInfoParts[1], "**********");
481                 }
482             }
483         } catch (URISyntaxException e) {
484             // Just return what was passed in
485         }
486         return sanitizedUrl;
487     }
488
489     /**
490      * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
491      */
492     private State createImage(RawType image) {
493         if (image == null) {
494             return UnDefType.UNDEF;
495         } else {
496             return image;
497         }
498     }
499
500     @Override
501     public void yearChangeEvent(String mac, String year) {
502         updateChannel(mac, CHANNEL_YEAR, new StringType(year));
503     }
504
505     @Override
506     public void genreChangeEvent(String mac, String genre) {
507         updateChannel(mac, CHANNEL_GENRE, new StringType(genre));
508     }
509
510     @Override
511     public void remoteTitleChangeEvent(String mac, String title) {
512         updateChannel(mac, CHANNEL_REMOTE_TITLE, new StringType(title));
513     }
514
515     @Override
516     public void irCodeChangeEvent(String mac, String ircode) {
517         if (isMe(mac)) {
518             postCommand(CHANNEL_IRCODE, new StringType(ircode));
519         }
520     }
521
522     @Override
523     public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
524         if (isMe(mac)) {
525             this.likeCommand = likeCommand;
526             this.unlikeCommand = unlikeCommand;
527             logger.trace("Player {} got a button change event: like='{}' unlike='{}'", mac, likeCommand, unlikeCommand);
528         }
529     }
530
531     @Override
532     public void updateFavoritesListEvent(List<Favorite> favorites) {
533         logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size());
534         List<StateOption> options = new ArrayList<>();
535         for (Favorite favorite : favorites) {
536             options.add(new StateOption(favorite.shortId, favorite.name));
537         }
538         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FAVORITES_PLAY), options);
539     }
540
541     /**
542      * Update a channel if the mac matches our own
543      *
544      * @param mac
545      * @param channelID
546      * @param state
547      */
548     private void updateChannel(String mac, String channelID, State state) {
549         if (isMe(mac)) {
550             State prevState = stateMap.put(channelID, state);
551             if (prevState == null || !prevState.equals(state)) {
552                 logger.trace("Updating channel {} for thing {} with mac {} to state {}", channelID, getThing().getUID(),
553                         mac, state);
554                 updateState(channelID, state);
555             }
556         }
557     }
558
559     /**
560      * Helper methods to get the current state of the player
561      *
562      * @return
563      */
564     int currentVolume() {
565         if (stateMap.containsKey(CHANNEL_VOLUME)) {
566             return ((DecimalType) stateMap.get(CHANNEL_VOLUME)).intValue();
567         } else {
568             return 0;
569         }
570     }
571
572     int currentPlayingTime() {
573         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYING_TIME)) {
574             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYING_TIME)).intValue();
575         } else {
576             return 0;
577         }
578     }
579
580     int currentNumberPlaylistTracks() {
581         if (stateMap.containsKey(CHANNEL_NUMBER_PLAYLIST_TRACKS)) {
582             return ((DecimalType) stateMap.get(CHANNEL_NUMBER_PLAYLIST_TRACKS)).intValue();
583         } else {
584             return 0;
585         }
586     }
587
588     int currentPlaylistIndex() {
589         if (stateMap.containsKey(CHANNEL_PLAYLIST_INDEX)) {
590             return ((DecimalType) stateMap.get(CHANNEL_PLAYLIST_INDEX)).intValue();
591         } else {
592             return 0;
593         }
594     }
595
596     boolean currentPower() {
597         if (stateMap.containsKey(CHANNEL_POWER)) {
598             return (stateMap.get(CHANNEL_POWER).equals(OnOffType.ON) ? true : false);
599         } else {
600             return false;
601         }
602     }
603
604     boolean currentStop() {
605         if (stateMap.containsKey(CHANNEL_STOP)) {
606             return (stateMap.get(CHANNEL_STOP).equals(OnOffType.ON) ? true : false);
607         } else {
608             return false;
609         }
610     }
611
612     boolean currentControl() {
613         if (stateMap.containsKey(CHANNEL_CONTROL)) {
614             return (stateMap.get(CHANNEL_CONTROL).equals(PlayPauseType.PLAY) ? true : false);
615         } else {
616             return false;
617         }
618     }
619
620     boolean currentMute() {
621         if (stateMap.containsKey(CHANNEL_MUTE)) {
622             return (stateMap.get(CHANNEL_MUTE).equals(OnOffType.ON) ? true : false);
623         } else {
624             return false;
625         }
626     }
627
628     int currentShuffle() {
629         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)) {
630             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)).intValue();
631         } else {
632             return 0;
633         }
634     }
635
636     int currentRepeat() {
637         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_REPEAT)) {
638             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_REPEAT)).intValue();
639         } else {
640             return 0;
641         }
642     }
643
644     /**
645      * Ticks away when in a play state to keep current track time
646      */
647     private void timeCounter() {
648         timeCounterJob = scheduler.scheduleWithFixedDelay(() -> {
649             if (playing) {
650                 updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(currentTime++));
651             }
652         }, 0, 1, TimeUnit.SECONDS);
653     }
654
655     private boolean isMe(String mac) {
656         return mac.equals(this.mac);
657     }
658
659     /**
660      * Returns our server handler if set
661      *
662      * @return
663      */
664     public SqueezeBoxServerHandler getSqueezeBoxServerHandler() {
665         return this.squeezeBoxServerHandler;
666     }
667
668     /**
669      * Returns the MAC address for this player
670      *
671      * @return
672      */
673     public String getMac() {
674         return this.mac;
675     }
676
677     /*
678      * Give the notification player access to the notification timeout
679      */
680     public int getNotificationTimeout() {
681         return getConfigAs(SqueezeBoxPlayerConfig.class).notificationTimeout;
682     }
683
684     /*
685      * Used by the AudioSink to get the volume level that should be used for the notification.
686      * Priority for determining volume is:
687      * - volume is provided in the say/playSound actions
688      * - volume is contained in the player thing's configuration
689      * - current player volume setting
690      */
691     public PercentType getNotificationSoundVolume() {
692         // Get the notification sound volume from this player thing's configuration
693         Integer configNotificationSoundVolume = getConfigAs(SqueezeBoxPlayerConfig.class).notificationVolume;
694
695         // Determine which volume to use
696         Integer currentNotificationSoundVolume;
697         if (notificationSoundVolume != null) {
698             currentNotificationSoundVolume = notificationSoundVolume;
699         } else if (configNotificationSoundVolume != null) {
700             currentNotificationSoundVolume = configNotificationSoundVolume;
701         } else {
702             currentNotificationSoundVolume = Integer.valueOf(currentVolume());
703         }
704         return new PercentType(currentNotificationSoundVolume.intValue());
705     }
706
707     /*
708      * Used by the AudioSink to set the volume level that should be used to play the notification
709      */
710     public void setNotificationSoundVolume(PercentType newNotificationSoundVolume) {
711         if (newNotificationSoundVolume != null) {
712             notificationSoundVolume = Integer.valueOf(newNotificationSoundVolume.intValue());
713         }
714     }
715
716     /*
717      * Play the notification.
718      */
719     public void playNotificationSoundURI(StringType uri) {
720         logger.debug("Play notification sound on player {} at URI {}", mac, uri);
721
722         try (SqueezeBoxNotificationPlayer notificationPlayer = new SqueezeBoxNotificationPlayer(this,
723                 squeezeBoxServerHandler, uri)) {
724             notificationPlayer.play();
725         } catch (InterruptedException e) {
726             logger.warn("Notification playback was interrupted", e);
727         } catch (SqueezeBoxTimeoutException e) {
728             logger.debug("SqueezeBoxTimeoutException during notification: {}", e.getMessage());
729         } finally {
730             notificationSoundVolume = null;
731         }
732     }
733
734     /*
735      * Return the IP and port of the OH2 web server
736      */
737     public String getHostAndPort() {
738         return callbackUrl;
739     }
740 }