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