]> git.basschouten.com Git - openhab-addons.git/blob
a29082516b334280de325420fe7f5acbce8f016e
[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.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.apache.commons.lang.StringUtils;
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 (StringUtils.isBlank(command.toString())) {
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             default:
312                 break;
313         }
314     }
315
316     @Override
317     public void playerAdded(SqueezeBoxPlayer player) {
318         // Player properties are saved in SqueezeBoxPlayerDiscoveryParticipant
319     }
320
321     @Override
322     public void powerChangeEvent(String mac, boolean power) {
323         updateChannel(mac, CHANNEL_POWER, power ? OnOffType.ON : OnOffType.OFF);
324         if (!power && isMe(mac)) {
325             playing = false;
326         }
327     }
328
329     @Override
330     public synchronized void modeChangeEvent(String mac, String mode) {
331         updateChannel(mac, CHANNEL_CONTROL, "play".equals(mode) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
332         updateChannel(mac, CHANNEL_PLAY_PAUSE, "play".equals(mode) ? OnOffType.ON : OnOffType.OFF);
333         updateChannel(mac, CHANNEL_STOP, "stop".equals(mode) ? OnOffType.ON : OnOffType.OFF);
334         if (isMe(mac)) {
335             playing = "play".equalsIgnoreCase(mode);
336         }
337     }
338
339     @Override
340     public void sourceChangeEvent(String mac, String source) {
341         updateChannel(mac, CHANNEL_SOURCE, StringType.valueOf(source));
342     }
343
344     @Override
345     public void absoluteVolumeChangeEvent(String mac, int volume) {
346         int newVolume = volume;
347         newVolume = Math.min(100, newVolume);
348         newVolume = Math.max(0, newVolume);
349         updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
350     }
351
352     @Override
353     public void relativeVolumeChangeEvent(String mac, int volumeChange) {
354         int newVolume = currentVolume() + volumeChange;
355         newVolume = Math.min(100, newVolume);
356         newVolume = Math.max(0, newVolume);
357         updateChannel(mac, CHANNEL_VOLUME, new PercentType(newVolume));
358
359         if (isMe(mac)) {
360             logger.trace("Volume changed [{}] for player {}. New volume: {}", volumeChange, mac, newVolume);
361         }
362     }
363
364     @Override
365     public void muteChangeEvent(String mac, boolean mute) {
366         updateChannel(mac, CHANNEL_MUTE, mute ? OnOffType.ON : OnOffType.OFF);
367     }
368
369     @Override
370     public void currentPlaylistIndexEvent(String mac, int index) {
371         updateChannel(mac, CHANNEL_PLAYLIST_INDEX, new DecimalType(index));
372     }
373
374     @Override
375     public void currentPlayingTimeEvent(String mac, int time) {
376         updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(time));
377         if (isMe(mac)) {
378             currentTime = time;
379         }
380     }
381
382     @Override
383     public void durationEvent(String mac, int duration) {
384         if (getThing().getChannel(CHANNEL_DURATION) == null) {
385             logger.debug("Channel 'duration' does not exist.  Delete and readd player thing to pick up channel.");
386             return;
387         }
388         updateChannel(mac, CHANNEL_DURATION, new DecimalType(duration));
389     }
390
391     @Override
392     public void numberPlaylistTracksEvent(String mac, int track) {
393         updateChannel(mac, CHANNEL_NUMBER_PLAYLIST_TRACKS, new DecimalType(track));
394     }
395
396     @Override
397     public void currentPlaylistShuffleEvent(String mac, int shuffle) {
398         updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_SHUFFLE, new DecimalType(shuffle));
399     }
400
401     @Override
402     public void currentPlaylistRepeatEvent(String mac, int repeat) {
403         updateChannel(mac, CHANNEL_CURRENT_PLAYLIST_REPEAT, new DecimalType(repeat));
404     }
405
406     @Override
407     public void titleChangeEvent(String mac, String title) {
408         updateChannel(mac, CHANNEL_TITLE, new StringType(title));
409     }
410
411     @Override
412     public void albumChangeEvent(String mac, String album) {
413         updateChannel(mac, CHANNEL_ALBUM, new StringType(album));
414     }
415
416     @Override
417     public void artistChangeEvent(String mac, String artist) {
418         updateChannel(mac, CHANNEL_ARTIST, new StringType(artist));
419     }
420
421     @Override
422     public void coverArtChangeEvent(String mac, String coverArtUrl) {
423         updateChannel(mac, CHANNEL_COVERART_DATA, createImage(downloadImage(mac, coverArtUrl)));
424     }
425
426     /**
427      * Download and cache the image data from an URL.
428      *
429      * @param url The URL of the image to be downloaded.
430      * @return A RawType object containing the image, null if the content type could not be found or the content type is
431      *         not an image.
432      */
433     private RawType downloadImage(String mac, String url) {
434         // Only get the image if this is my PlayerHandler instance
435         if (isMe(mac)) {
436             if (StringUtils.isNotEmpty(url)) {
437                 String sanitizedUrl = sanitizeUrl(url);
438                 RawType image = IMAGE_CACHE.putIfAbsentAndGet(url, () -> {
439                     logger.debug("Trying to download the content of URL {}", sanitizedUrl);
440                     try {
441                         return HttpUtil.downloadImage(url);
442                     } catch (IllegalArgumentException e) {
443                         logger.debug("IllegalArgumentException when downloading image from {}", sanitizedUrl, e);
444                         return null;
445                     }
446                 });
447                 if (image == null) {
448                     logger.debug("Failed to download the content of URL {}", sanitizedUrl);
449                     return null;
450                 } else {
451                     return image;
452                 }
453             }
454         }
455         return null;
456     }
457
458     /*
459      * Replaces the password in the URL, if present
460      */
461     private String sanitizeUrl(String url) {
462         String sanitizedUrl = url;
463         try {
464             URI uri = new URI(url);
465             String userInfo = uri.getUserInfo();
466             if (userInfo != null) {
467                 String[] userInfoParts = userInfo.split(":");
468                 if (userInfoParts.length == 2) {
469                     sanitizedUrl = url.replace(userInfoParts[1], "**********");
470                 }
471             }
472         } catch (URISyntaxException e) {
473             // Just return what was passed in
474         }
475         return sanitizedUrl;
476     }
477
478     /**
479      * Wrap the given RawType and return it as {@link State} or return {@link UnDefType#UNDEF} if the RawType is null.
480      */
481     private State createImage(RawType image) {
482         if (image == null) {
483             return UnDefType.UNDEF;
484         } else {
485             return image;
486         }
487     }
488
489     @Override
490     public void yearChangeEvent(String mac, String year) {
491         updateChannel(mac, CHANNEL_YEAR, new StringType(year));
492     }
493
494     @Override
495     public void genreChangeEvent(String mac, String genre) {
496         updateChannel(mac, CHANNEL_GENRE, new StringType(genre));
497     }
498
499     @Override
500     public void remoteTitleChangeEvent(String mac, String title) {
501         updateChannel(mac, CHANNEL_REMOTE_TITLE, new StringType(title));
502     }
503
504     @Override
505     public void irCodeChangeEvent(String mac, String ircode) {
506         if (isMe(mac)) {
507             postCommand(CHANNEL_IRCODE, new StringType(ircode));
508         }
509     }
510
511     @Override
512     public void buttonsChangeEvent(String mac, String likeCommand, String unlikeCommand) {
513         if (isMe(mac)) {
514             this.likeCommand = likeCommand;
515             this.unlikeCommand = unlikeCommand;
516             logger.trace("Player {} got a button change event: like='{}' unlike='{}'", mac, likeCommand, unlikeCommand);
517         }
518     }
519
520     @Override
521     public void updateFavoritesListEvent(List<Favorite> favorites) {
522         logger.trace("Player {} updating favorites list with {} favorites", mac, favorites.size());
523         List<StateOption> options = new ArrayList<>();
524         for (Favorite favorite : favorites) {
525             options.add(new StateOption(favorite.shortId, favorite.name));
526         }
527         stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_FAVORITES_PLAY), options);
528     }
529
530     /**
531      * Update a channel if the mac matches our own
532      *
533      * @param mac
534      * @param channelID
535      * @param state
536      */
537     private void updateChannel(String mac, String channelID, State state) {
538         if (isMe(mac)) {
539             State prevState = stateMap.put(channelID, state);
540             if (prevState == null || !prevState.equals(state)) {
541                 logger.trace("Updating channel {} for thing {} with mac {} to state {}", channelID, getThing().getUID(),
542                         mac, state);
543                 updateState(channelID, state);
544             }
545         }
546     }
547
548     /**
549      * Helper methods to get the current state of the player
550      *
551      * @return
552      */
553     int currentVolume() {
554         if (stateMap.containsKey(CHANNEL_VOLUME)) {
555             return ((DecimalType) stateMap.get(CHANNEL_VOLUME)).intValue();
556         } else {
557             return 0;
558         }
559     }
560
561     int currentPlayingTime() {
562         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYING_TIME)) {
563             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYING_TIME)).intValue();
564         } else {
565             return 0;
566         }
567     }
568
569     int currentNumberPlaylistTracks() {
570         if (stateMap.containsKey(CHANNEL_NUMBER_PLAYLIST_TRACKS)) {
571             return ((DecimalType) stateMap.get(CHANNEL_NUMBER_PLAYLIST_TRACKS)).intValue();
572         } else {
573             return 0;
574         }
575     }
576
577     int currentPlaylistIndex() {
578         if (stateMap.containsKey(CHANNEL_PLAYLIST_INDEX)) {
579             return ((DecimalType) stateMap.get(CHANNEL_PLAYLIST_INDEX)).intValue();
580         } else {
581             return 0;
582         }
583     }
584
585     boolean currentPower() {
586         if (stateMap.containsKey(CHANNEL_POWER)) {
587             return (stateMap.get(CHANNEL_POWER).equals(OnOffType.ON) ? true : false);
588         } else {
589             return false;
590         }
591     }
592
593     boolean currentStop() {
594         if (stateMap.containsKey(CHANNEL_STOP)) {
595             return (stateMap.get(CHANNEL_STOP).equals(OnOffType.ON) ? true : false);
596         } else {
597             return false;
598         }
599     }
600
601     boolean currentControl() {
602         if (stateMap.containsKey(CHANNEL_CONTROL)) {
603             return (stateMap.get(CHANNEL_CONTROL).equals(PlayPauseType.PLAY) ? true : false);
604         } else {
605             return false;
606         }
607     }
608
609     boolean currentMute() {
610         if (stateMap.containsKey(CHANNEL_MUTE)) {
611             return (stateMap.get(CHANNEL_MUTE).equals(OnOffType.ON) ? true : false);
612         } else {
613             return false;
614         }
615     }
616
617     int currentShuffle() {
618         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)) {
619             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_SHUFFLE)).intValue();
620         } else {
621             return 0;
622         }
623     }
624
625     int currentRepeat() {
626         if (stateMap.containsKey(CHANNEL_CURRENT_PLAYLIST_REPEAT)) {
627             return ((DecimalType) stateMap.get(CHANNEL_CURRENT_PLAYLIST_REPEAT)).intValue();
628         } else {
629             return 0;
630         }
631     }
632
633     /**
634      * Ticks away when in a play state to keep current track time
635      */
636     private void timeCounter() {
637         timeCounterJob = scheduler.scheduleWithFixedDelay(() -> {
638             if (playing) {
639                 updateChannel(mac, CHANNEL_CURRENT_PLAYING_TIME, new DecimalType(currentTime++));
640             }
641         }, 0, 1, TimeUnit.SECONDS);
642     }
643
644     private boolean isMe(String mac) {
645         return mac.equals(this.mac);
646     }
647
648     /**
649      * Returns our server handler if set
650      *
651      * @return
652      */
653     public SqueezeBoxServerHandler getSqueezeBoxServerHandler() {
654         return this.squeezeBoxServerHandler;
655     }
656
657     /**
658      * Returns the MAC address for this player
659      *
660      * @return
661      */
662     public String getMac() {
663         return this.mac;
664     }
665
666     /*
667      * Give the notification player access to the notification timeout
668      */
669     public int getNotificationTimeout() {
670         return getConfigAs(SqueezeBoxPlayerConfig.class).notificationTimeout;
671     }
672
673     /*
674      * Used by the AudioSink to get the volume level that should be used for the notification.
675      * Priority for determining volume is:
676      * - volume is provided in the say/playSound actions
677      * - volume is contained in the player thing's configuration
678      * - current player volume setting
679      */
680     public PercentType getNotificationSoundVolume() {
681         // Get the notification sound volume from this player thing's configuration
682         Integer configNotificationSoundVolume = getConfigAs(SqueezeBoxPlayerConfig.class).notificationVolume;
683
684         // Determine which volume to use
685         Integer currentNotificationSoundVolume;
686         if (notificationSoundVolume != null) {
687             currentNotificationSoundVolume = notificationSoundVolume;
688         } else if (configNotificationSoundVolume != null) {
689             currentNotificationSoundVolume = configNotificationSoundVolume;
690         } else {
691             currentNotificationSoundVolume = Integer.valueOf(currentVolume());
692         }
693         return new PercentType(currentNotificationSoundVolume.intValue());
694     }
695
696     /*
697      * Used by the AudioSink to set the volume level that should be used to play the notification
698      */
699     public void setNotificationSoundVolume(PercentType newNotificationSoundVolume) {
700         if (newNotificationSoundVolume != null) {
701             notificationSoundVolume = Integer.valueOf(newNotificationSoundVolume.intValue());
702         }
703     }
704
705     /*
706      * Play the notification.
707      */
708     public void playNotificationSoundURI(StringType uri) {
709         logger.debug("Play notification sound on player {} at URI {}", mac, uri);
710
711         try (SqueezeBoxNotificationPlayer notificationPlayer = new SqueezeBoxNotificationPlayer(this,
712                 squeezeBoxServerHandler, uri)) {
713             notificationPlayer.play();
714         } catch (InterruptedException e) {
715             logger.warn("Notification playback was interrupted", e);
716         } catch (SqueezeBoxTimeoutException e) {
717             logger.debug("SqueezeBoxTimeoutException during notification: {}", e.getMessage());
718         } finally {
719             notificationSoundVolume = null;
720         }
721     }
722
723     /*
724      * Return the IP and port of the OH2 web server
725      */
726     public String getHostAndPort() {
727         return callbackUrl;
728     }
729 }