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