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