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