2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.upnpcontrol.internal.handler;
15 import static org.eclipse.jdt.annotation.Checks.requireNonNull;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.Matchers.is;
18 import static org.junit.jupiter.api.Assertions.assertNull;
19 import static org.mockito.ArgumentMatchers.*;
20 import static org.mockito.Mockito.*;
21 import static org.openhab.binding.upnpcontrol.internal.UpnpControlBindingConstants.*;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
28 import java.util.concurrent.TimeUnit;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.junit.jupiter.api.AfterEach;
33 import org.junit.jupiter.api.BeforeEach;
34 import org.junit.jupiter.api.Test;
35 import org.mockito.ArgumentCaptor;
36 import org.mockito.Mock;
37 import org.openhab.binding.upnpcontrol.internal.audiosink.UpnpAudioSinkReg;
38 import org.openhab.binding.upnpcontrol.internal.config.UpnpControlRendererConfiguration;
39 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntry;
40 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryQueue;
41 import org.openhab.binding.upnpcontrol.internal.queue.UpnpEntryRes;
42 import org.openhab.binding.upnpcontrol.internal.util.UpnpXMLParser;
43 import org.openhab.core.library.types.DecimalType;
44 import org.openhab.core.library.types.NextPreviousType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.PercentType;
47 import org.openhab.core.library.types.PlayPauseType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.Units;
51 import org.openhab.core.thing.Channel;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingUID;
55 import org.openhab.core.thing.binding.builder.ChannelBuilder;
56 import org.openhab.core.types.Command;
57 import org.openhab.core.types.CommandOption;
58 import org.openhab.core.types.State;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * Unit tests for {@link UpnpRendererHandler}.
66 * @author Mark Herwege - Initial contribution
68 @SuppressWarnings({ "null", "unchecked" })
70 public class UpnpRendererHandlerTest extends UpnpHandlerTest {
72 private final Logger logger = LoggerFactory.getLogger(UpnpRendererHandlerTest.class);
74 private static final String THING_TYPE_UID = "upnpcontrol:upnprenderer";
75 private static final String THING_UID = THING_TYPE_UID + ":mockrenderer";
77 private static final String LAST_CHANGE_HEADER = """
78 <Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">\
81 private static final String LAST_CHANGE_FOOTER = "</InstanceID></Event>";
82 private static final String AV_TRANSPORT_URI = "<AVTransportURI val=\"";
83 private static final String AV_TRANSPORT_URI_METADATA = "<AVTransportURIMetaData val=\"";
84 private static final String CURRENT_TRACK_URI = "<CurrentTrackURI val=\"";
85 private static final String CURRENT_TRACK_METADATA = "<CurrentTrackMetaData val=\"";
86 private static final String TRANSPORT_STATE = "<TransportState val=\"";
87 private static final String CLOSE = "\"/>";
89 protected @Nullable UpnpRendererHandler handler;
91 private @Nullable UpnpEntryQueue upnpEntryQueue;
93 private ChannelUID volumeChannelUID = new ChannelUID(THING_UID + ":" + VOLUME);
94 private Channel volumeChannel = ChannelBuilder.create(volumeChannelUID, "Dimmer").build();
96 private ChannelUID muteChannelUID = new ChannelUID(THING_UID + ":" + MUTE);
97 private Channel muteChannel = ChannelBuilder.create(muteChannelUID, "Switch").build();
99 private ChannelUID stopChannelUID = new ChannelUID(THING_UID + ":" + STOP);
100 private Channel stopChannel = ChannelBuilder.create(stopChannelUID, "Switch").build();
102 private ChannelUID controlChannelUID = new ChannelUID(THING_UID + ":" + CONTROL);
103 private Channel controlChannel = ChannelBuilder.create(controlChannelUID, "Player").build();
105 private ChannelUID repeatChannelUID = new ChannelUID(THING_UID + ":" + REPEAT);
106 private Channel repeatChannel = ChannelBuilder.create(repeatChannelUID, "Switch").build();
108 private ChannelUID shuffleChannelUID = new ChannelUID(THING_UID + ":" + SHUFFLE);
109 private Channel shuffleChannel = ChannelBuilder.create(shuffleChannelUID, "Switch").build();
111 private ChannelUID onlyPlayOneChannelUID = new ChannelUID(THING_UID + ":" + ONLY_PLAY_ONE);
112 private Channel onlyPlayOneChannel = ChannelBuilder.create(onlyPlayOneChannelUID, "Switch").build();
114 private ChannelUID uriChannelUID = new ChannelUID(THING_UID + ":" + URI);
115 private Channel uriChannel = ChannelBuilder.create(uriChannelUID, "String").build();
117 private ChannelUID favoriteSelectChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_SELECT);
118 private Channel favoriteSelectChannel = ChannelBuilder.create(favoriteSelectChannelUID, "String").build();
120 private ChannelUID favoriteChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE);
121 private Channel favoriteChannel = ChannelBuilder.create(favoriteChannelUID, "String").build();
123 private ChannelUID favoriteActionChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_ACTION);
124 private Channel favoriteActionChannel = ChannelBuilder.create(favoriteActionChannelUID, "String").build();
126 private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
127 private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
129 private ChannelUID titleChannelUID = new ChannelUID(THING_UID + ":" + TITLE);
130 private Channel titleChannel = ChannelBuilder.create(titleChannelUID, "String").build();
132 private ChannelUID albumChannelUID = new ChannelUID(THING_UID + ":" + ALBUM);
133 private Channel albumChannel = ChannelBuilder.create(albumChannelUID, "String").build();
135 private ChannelUID albumArtChannelUID = new ChannelUID(THING_UID + ":" + ALBUM_ART);
136 private Channel albumArtChannel = ChannelBuilder.create(albumArtChannelUID, "Image").build();
138 private ChannelUID creatorChannelUID = new ChannelUID(THING_UID + ":" + CREATOR);
139 private Channel creatorChannel = ChannelBuilder.create(creatorChannelUID, "String").build();
141 private ChannelUID artistChannelUID = new ChannelUID(THING_UID + ":" + ARTIST);
142 private Channel artistChannel = ChannelBuilder.create(artistChannelUID, "String").build();
144 private ChannelUID publisherChannelUID = new ChannelUID(THING_UID + ":" + PUBLISHER);
145 private Channel publisherChannel = ChannelBuilder.create(publisherChannelUID, "String").build();
147 private ChannelUID genreChannelUID = new ChannelUID(THING_UID + ":" + GENRE);
148 private Channel genreChannel = ChannelBuilder.create(genreChannelUID, "String").build();
150 private ChannelUID trackNumberChannelUID = new ChannelUID(THING_UID + ":" + TRACK_NUMBER);
151 private Channel trackNumberChannel = ChannelBuilder.create(trackNumberChannelUID, "Number").build();
153 private ChannelUID trackDurationChannelUID = new ChannelUID(THING_UID + ":" + TRACK_DURATION);
154 private Channel trackDurationChannel = ChannelBuilder.create(trackDurationChannelUID, "Number:Time").build();
156 private ChannelUID trackPositionChannelUID = new ChannelUID(THING_UID + ":" + TRACK_POSITION);
157 private Channel trackPositionChannel = ChannelBuilder.create(trackPositionChannelUID, "Number:Time").build();
159 private ChannelUID relTrackPositionChannelUID = new ChannelUID(THING_UID + ":" + REL_TRACK_POSITION);
160 private Channel relTrackPositionChannel = ChannelBuilder.create(relTrackPositionChannelUID, "Dimmer").build();
163 private @Nullable UpnpAudioSinkReg audioSinkReg;
167 public void setUp() {
170 // stub thing methods
171 when(thing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnprenderer", "mockrenderer"));
172 when(thing.getLabel()).thenReturn("MockRenderer");
173 when(thing.getStatus()).thenReturn(ThingStatus.OFFLINE);
176 when(thing.getChannel(VOLUME)).thenReturn(volumeChannel);
177 when(thing.getChannel(MUTE)).thenReturn(muteChannel);
178 when(thing.getChannel(STOP)).thenReturn(stopChannel);
179 when(thing.getChannel(CONTROL)).thenReturn(controlChannel);
180 when(thing.getChannel(REPEAT)).thenReturn(repeatChannel);
181 when(thing.getChannel(SHUFFLE)).thenReturn(shuffleChannel);
182 when(thing.getChannel(ONLY_PLAY_ONE)).thenReturn(onlyPlayOneChannel);
183 when(thing.getChannel(URI)).thenReturn(uriChannel);
184 when(thing.getChannel(FAVORITE_SELECT)).thenReturn(favoriteSelectChannel);
185 when(thing.getChannel(FAVORITE)).thenReturn(favoriteChannel);
186 when(thing.getChannel(FAVORITE_ACTION)).thenReturn(favoriteActionChannel);
187 when(thing.getChannel(PLAYLIST_SELECT)).thenReturn(playlistSelectChannel);
188 when(thing.getChannel(TITLE)).thenReturn(titleChannel);
189 when(thing.getChannel(ALBUM)).thenReturn(albumChannel);
190 when(thing.getChannel(ALBUM_ART)).thenReturn(albumArtChannel);
191 when(thing.getChannel(CREATOR)).thenReturn(creatorChannel);
192 when(thing.getChannel(ARTIST)).thenReturn(artistChannel);
193 when(thing.getChannel(PUBLISHER)).thenReturn(publisherChannel);
194 when(thing.getChannel(GENRE)).thenReturn(genreChannel);
195 when(thing.getChannel(TRACK_NUMBER)).thenReturn(trackNumberChannel);
196 when(thing.getChannel(TRACK_DURATION)).thenReturn(trackDurationChannel);
197 when(thing.getChannel(TRACK_POSITION)).thenReturn(trackPositionChannel);
198 when(thing.getChannel(REL_TRACK_POSITION)).thenReturn(relTrackPositionChannel);
200 // stub config for initialize
201 when(config.as(UpnpControlRendererConfiguration.class)).thenReturn(new UpnpControlRendererConfiguration());
203 // create a media queue for playing
204 List<UpnpEntry> entries = createUpnpEntries();
205 upnpEntryQueue = new UpnpEntryQueue(entries, "54321");
207 handler = spy(new UpnpRendererHandler(requireNonNull(thing), requireNonNull(upnpIOService),
208 requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider),
209 requireNonNull(upnpCommandDescriptionProvider), configuration));
211 initHandler(requireNonNull(handler));
213 handler.initialize();
215 expectLastChangeOnStop(true);
216 expectLastChangeOnPlay(true);
217 expectLastChangeOnPause(true);
220 private List<UpnpEntry> createUpnpEntries() {
221 List<UpnpEntry> entries = new ArrayList<>();
223 List<UpnpEntryRes> resList;
225 resList = new ArrayList<>();
226 res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 8054458L, "10", "http://MediaServerContent_0/1/M0/");
227 res.setRes("http://MediaServerContent_0/1/M0/Test_0.mp3");
229 entry = new UpnpEntry("M0", "M0", "C11", "object.item.audioItem").withTitle("Music_00").withResList(resList)
230 .withAlbum("My Music 0").withCreator("Creator_0").withArtist("Artist_0").withGenre("Morning")
231 .withPublisher("myself 0").withAlbumArtUri("").withTrackNumber(1);
233 resList = new ArrayList<>();
234 res = new UpnpEntryRes("http-get:*:audio/wav:*", 1156598L, "6", "http://MediaServerContent_0/1/M1/");
235 res.setRes("http://MediaServerContent_0/1/M1/Test_1.wav");
237 entry = new UpnpEntry("M1", "M1", "C11", "object.item.audioItem").withTitle("Music_01").withResList(resList)
238 .withAlbum("My Music 0").withCreator("Creator_1").withArtist("Artist_1").withGenre("Morning")
239 .withPublisher("myself 1").withAlbumArtUri("").withTrackNumber(2);
241 resList = new ArrayList<>();
242 res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 1156598L, "40", "http://MediaServerContent_0/1/M2/");
243 res.setRes("http://MediaServerContent_0/1/M2/Test_2.mp3");
245 entry = new UpnpEntry("M2", "M2", "C12", "object.item.audioItem").withTitle("Music_02").withResList(resList)
246 .withAlbum("My Music 2").withCreator("Creator_2").withArtist("Artist_2").withGenre("Evening")
247 .withPublisher("myself 2").withAlbumArtUri("").withTrackNumber(1);
254 public void tearDown() {
261 public void testRegisterQueue() {
262 logger.info("testRegisterQueue");
264 // Register a media queue
265 expectLastChangeOnSetAVTransportURI(true, 0);
266 handler.registerQueue(requireNonNull(upnpEntryQueue));
268 checkInternalState(0, 1, true, false, true, false);
269 checkControlChannel(PlayPauseType.PAUSE);
271 checkMetadataChannels(0);
275 public void testPlayQueue() {
276 logger.info("testPlayQueue");
278 // Register a media queue
279 expectLastChangeOnSetAVTransportURI(true, 0);
280 handler.registerQueue(requireNonNull(upnpEntryQueue));
283 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
285 checkInternalState(0, 1, false, true, false, true);
286 checkControlChannel(PlayPauseType.PLAY);
288 checkMetadataChannels(0);
292 public void testStop() {
293 logger.info("testStop");
295 // Register a media queue
296 expectLastChangeOnSetAVTransportURI(true, 0);
297 handler.registerQueue(requireNonNull(upnpEntryQueue));
300 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
303 handler.handleCommand(stopChannelUID, OnOffType.ON);
305 checkInternalState(0, 1, true, false, false, false);
306 checkControlChannel(PlayPauseType.PAUSE);
308 checkMetadataChannels(0);
312 public void testPause() {
313 logger.info("testPause");
315 // Register a media queue
316 expectLastChangeOnSetAVTransportURI(true, 0);
317 handler.registerQueue(requireNonNull(upnpEntryQueue));
320 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
323 handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
325 checkControlChannel(PlayPauseType.PAUSE);
328 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
330 checkControlChannel(PlayPauseType.PLAY);
334 public void testPauseNotSupported() {
335 logger.info("testPauseNotSupported");
337 // Some players don't support pause and just continue playing.
338 // Test if we properly switch back to playing state if no confirmation of pause received.
340 // Register a media queue
341 expectLastChangeOnSetAVTransportURI(true, 0);
342 handler.registerQueue(requireNonNull(upnpEntryQueue));
345 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
348 // Do not receive a PAUSED_PLAYBACK response
349 expectLastChangeOnPause(false);
350 handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
352 // Wait long enough for status to turn back to PLAYING.
353 // All timeouts in test are set to 1s.
355 TimeUnit.SECONDS.sleep(1);
356 } catch (InterruptedException ignore) {
359 checkControlChannel(PlayPauseType.PLAY);
363 public void testRegisterQueueWhilePlaying() {
364 logger.info("testRegisterQueueWhilePlaying");
366 // Register a media queue
367 expectLastChangeOnSetAVTransportURI(true, 2);
368 List<UpnpEntry> startList = new ArrayList<UpnpEntry>();
369 startList.add(requireNonNull(upnpEntryQueue.get(2)));
370 UpnpEntryQueue startQueue = new UpnpEntryQueue(startList, "54321");
371 handler.registerQueue(requireNonNull(startQueue));
374 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
376 // Register a new media queue
377 expectLastChangeOnSetAVTransportURI(true, 0);
378 handler.registerQueue(requireNonNull(upnpEntryQueue));
380 checkInternalState(2, 0, false, true, true, true);
381 checkControlChannel(PlayPauseType.PLAY);
382 checkSetURI(null, 0);
383 checkMetadataChannels(2);
387 public void testNext() {
388 logger.info("testNext");
390 testNext(false, false);
394 public void testNextRepeat() {
395 logger.info("testNextRepeat");
397 testNext(false, true);
401 public void testNextWhilePlaying() {
402 logger.info("testNextWhilePlaying");
404 testNext(true, false);
408 public void testNextWhilePlayingRepeat() {
409 logger.info("testNextWhilePlayingRepeat");
411 testNext(true, true);
414 private void testNext(boolean play, boolean repeat) {
415 // Register a media queue
416 expectLastChangeOnSetAVTransportURI(true, 0);
417 handler.registerQueue(requireNonNull(upnpEntryQueue));
420 handler.handleCommand(repeatChannelUID, OnOffType.ON);
425 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
429 expectLastChangeOnSetAVTransportURI(true, 1);
430 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
432 checkInternalState(1, 2, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
433 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
435 checkMetadataChannels(1);
438 expectLastChangeOnSetAVTransportURI(true, 2);
439 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
441 checkInternalState(2, repeat ? 0 : null, play ? false : true, play ? true : false, play ? false : true,
442 play ? true : false);
443 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
444 checkSetURI(2, repeat ? 0 : null);
445 checkMetadataChannels(2);
448 expectLastChangeOnSetAVTransportURI(true, 0);
449 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
451 checkInternalState(0, 1, (play && repeat) ? false : true, (play && repeat) ? true : false,
452 (play && repeat) ? false : true, (play && repeat) ? true : false);
453 checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
455 checkMetadataChannels(0);
459 public void testPrevious() {
460 logger.info("testPrevious");
462 testPrevious(false, false);
466 public void testPreviousRepeat() {
467 logger.info("testPreviousRepeat");
469 testPrevious(false, true);
473 public void testPreviousWhilePlaying() {
474 logger.info("testPreviousWhilePlaying");
476 testPrevious(true, false);
480 public void testPreviousWhilePlayingRepeat() {
481 logger.info("testPreviousWhilePlayingRepeat");
483 testPrevious(true, true);
486 public void testPrevious(boolean play, boolean repeat) {
487 // Register a media queue
488 expectLastChangeOnSetAVTransportURI(true, 0);
489 handler.registerQueue(requireNonNull(upnpEntryQueue));
492 handler.handleCommand(repeatChannelUID, OnOffType.ON);
497 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
501 expectLastChangeOnSetAVTransportURI(true, 1);
502 handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
505 expectLastChangeOnSetAVTransportURI(true, 2);
506 handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
508 checkInternalState(0, 1, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
509 checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
511 checkMetadataChannels(0);
514 expectLastChangeOnSetAVTransportURI(true, 0);
515 handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
517 checkInternalState(repeat ? 2 : 0, repeat ? 0 : 1, (play && repeat) ? false : true,
518 (play && repeat) ? true : false, (play && repeat) ? false : true, (play && repeat) ? true : false);
519 checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
520 checkSetURI(repeat ? 2 : 0, repeat ? 0 : 1);
521 checkMetadataChannels(repeat ? 2 : 0);
525 public void testAutoPlayNextInQueue() {
526 logger.info("testAutoPlayNextInQueue");
528 // Register a media queue
529 expectLastChangeOnSetAVTransportURI(true, 0);
530 handler.registerQueue(requireNonNull(upnpEntryQueue));
533 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
535 // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
536 expectLastChangeOnSetAVTransportURI(true, 1);
538 // At the end of the media, we will get GENA LastChange STOP event, renderer should move to next media and play
539 // Force this STOP event for test
540 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
541 handler.onValueReceived("LastChange", lastChange, "AVTransport");
543 checkInternalState(1, 2, false, true, false, true);
544 checkControlChannel(PlayPauseType.PLAY);
546 checkMetadataChannels(1);
550 public void testAutoPlayNextInQueueGapless() {
551 logger.info("testAutoPlayNextInQueueGapless");
553 // Register a media queue
554 expectLastChangeOnSetAVTransportURI(true, 0);
555 handler.registerQueue(requireNonNull(upnpEntryQueue));
558 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
560 // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
561 expectLastChangeOnSetAVTransportURI(true, 1);
563 // At the end of the media, we will get GENA event with new URI and metadata
564 String lastChange = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + upnpEntryQueue.get(1).getRes() + CLOSE
565 + AV_TRANSPORT_URI_METADATA + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(0)))
566 + CLOSE + CURRENT_TRACK_URI + upnpEntryQueue.get(1).getRes() + CLOSE + CURRENT_TRACK_METADATA
567 + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(1))) + CLOSE
568 + LAST_CHANGE_FOOTER;
569 handler.onValueReceived("LastChange", lastChange, "AVTransport");
571 checkInternalState(1, 2, false, true, false, true);
572 checkControlChannel(PlayPauseType.PLAY);
573 checkSetURI(null, 2);
574 checkMetadataChannels(1);
578 public void testOnlyPlayOne() {
579 logger.info("testOnlyPlayOne");
581 handler.handleCommand(onlyPlayOneChannelUID, OnOffType.ON);
583 // Register a media queue
584 expectLastChangeOnSetAVTransportURI(true, 0);
585 handler.registerQueue(requireNonNull(upnpEntryQueue));
588 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
590 checkInternalState(0, 1, false, true, false, true);
591 checkSetURI(0, null);
592 checkMetadataChannels(0);
594 // We expect GENA LastChange event with new metadata when the renderer has finished playing
595 expectLastChangeOnSetAVTransportURI(true, 1);
597 // At the end of the media, we will get GENA LastChange STOP event, renderer should stop
598 // Force this STOP event for test
599 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
600 handler.onValueReceived("LastChange", lastChange, "AVTransport");
602 checkInternalState(1, 2, false, false, false, true);
603 checkControlChannel(PlayPauseType.PAUSE);
604 checkSetURI(1, null);
605 checkMetadataChannels(1);
609 public void testPlayUri() {
610 logger.info("testPlayUri");
612 expectLastChangeOnSetAVTransportURI(true, false, 0);
613 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
616 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
618 checkInternalState(null, null, false, true, false, false);
619 checkControlChannel(PlayPauseType.PLAY);
620 checkSetURI(0, null, false);
621 checkMetadataChannels(0, true);
625 public void testPlayAction() {
626 logger.info("testPlayAction");
628 expectLastChangeOnSetAVTransportURI(true, false, 0);
630 // Methods called in sequence by audio sink
631 handler.setCurrentURI(upnpEntryQueue.get(0).getRes(), "");
634 checkInternalState(null, null, false, true, false, false);
635 checkControlChannel(PlayPauseType.PLAY);
636 checkSetURI(0, null, false);
637 checkMetadataChannels(0, true);
641 public void testPlayNotification() {
642 logger.info("testPlayNotification");
644 // Register a media queue
645 expectLastChangeOnSetAVTransportURI(true, 0);
646 handler.registerQueue(requireNonNull(upnpEntryQueue));
649 expectLastChangeOnSetVolume(true, 50);
650 handler.setVolume(new PercentType(50));
652 checkInternalState(0, 1, true, false, true, false);
653 checkSetURI(0, 1, true);
654 checkMetadataChannels(0, false);
656 // Play notification, at standard 10% volume above current volume level
657 expectLastChangeOnSetAVTransportURI(true, false, 2);
658 expectLastChangeOnGetPositionInfo(true, "00:00:00");
659 handler.playNotification(upnpEntryQueue.get(2).getRes());
661 checkInternalState(0, 1, true, false, true, false);
662 checkSetURI(2, null, false);
663 checkMetadataChannels(0, false);
664 verify(handler).setVolume(new PercentType(55));
666 // At the end of the notification, we will get GENA LastChange STOP event
667 // Force this STOP event for test
668 expectLastChangeOnSetAVTransportURI(true, false, 0);
669 String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
670 handler.onValueReceived("LastChange", lastChange, "AVTransport");
672 checkInternalState(0, 1, true, false, true, false);
673 checkMetadataChannels(0, false);
674 verify(handler, times(2)).setVolume(new PercentType(50));
676 // Play media and move to position
677 handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
679 checkInternalState(0, 1, false, true, false, true); //
680 checkSetURI(0, 1, true);
681 checkMetadataChannels(0, false);
683 // Play notification again, while simulating the current playing media is at 10s position
684 // Play at volume level provided by audiSink action
685 expectLastChangeOnSetAVTransportURI(true, false, 2);
686 expectLastChangeOnGetPositionInfo(true, "00:00:10");
687 handler.setNotificationVolume(new PercentType(70));
688 handler.playNotification(upnpEntryQueue.get(2).getRes());
690 checkInternalState(0, 1, false, true, false, true);
691 checkSetURI(2, null, false);
692 checkMetadataChannels(0, false);
693 verify(handler).setVolume(new PercentType(70));
695 // Wait long enough for max notification duration to be reached.
696 // In the test, we have enforced 500ms delay through schedule mock.
697 expectLastChangeOnSetAVTransportURI(true, false, 0);
699 TimeUnit.SECONDS.sleep(1);
700 logger.info("Test playing {}, stopped {}", handler.playing, handler.playerStopped);
701 } catch (InterruptedException ignore) {
704 checkInternalState(0, 1, false, true, false, true);
705 checkSetURI(0, null, false);
706 checkMetadataChannels(0, false);
707 verify(handler, times(3)).setVolume(new PercentType(50));
708 verify(callback, times(2)).stateUpdated(trackPositionChannelUID, new QuantityType<>(10, Units.SECOND));
712 public void testFavorite() {
713 logger.info("testFavorite");
715 // Check already called in initialize
716 verify(handler).updateFavoritesList();
719 expectLastChangeOnSetAVTransportURI(true, false, 0);
720 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
723 handler.handleCommand(favoriteChannelUID, StringType.valueOf("Test_Favorite"));
724 handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("SAVE"));
726 // Check called after saving favorite
727 verify(handler, times(2)).updateFavoritesList();
729 // Check that FAVORITE_SELECT channel now has the favorite as a state option
730 ArgumentCaptor<List<CommandOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
731 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
732 commandOptionListCaptor.capture());
733 assertThat(commandOptionListCaptor.getValue().size(), is(1));
734 assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("Test_Favorite"));
735 assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("Test_Favorite"));
737 // Clear FAVORITE channel
738 handler.handleCommand(favoriteChannelUID, StringType.valueOf(""));
741 expectLastChangeOnSetAVTransportURI(true, false, 2);
742 handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(2).getRes()));
744 checkInternalState(null, null, false, true, false, false);
745 checkSetURI(2, null, false);
746 checkMetadataChannels(2, true);
749 expectLastChangeOnSetAVTransportURI(true, false, 0);
750 handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
752 checkInternalState(null, null, false, true, false, false);
753 checkControlChannel(PlayPauseType.PLAY);
754 checkSetURI(0, null, false);
755 checkMetadataChannels(0, true);
758 handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
759 handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("DELETE"));
761 // Check called after deleting favorite
762 verify(handler, times(3)).updateFavoritesList();
764 // Check that FAVORITE_SELECT channel option list is empty again
765 commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
766 verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
767 commandOptionListCaptor.capture());
768 assertThat(commandOptionListCaptor.getValue().size(), is(0));
771 private void expectLastChangeOnStop(boolean respond) {
772 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
773 doAnswer(invocation -> {
775 handler.onValueReceived("LastChange", value, "AVTransport");
777 return Collections.emptyMap();
778 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Stop"), anyMap());
781 private void expectLastChangeOnPlay(boolean respond) {
782 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PLAYING" + CLOSE + LAST_CHANGE_FOOTER;
783 doAnswer(invocation -> {
785 handler.onValueReceived("LastChange", value, "AVTransport");
787 return Collections.emptyMap();
788 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Play"), anyMap());
791 private void expectLastChangeOnPause(boolean respond) {
792 String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PAUSED_PLAYBACK" + CLOSE + LAST_CHANGE_FOOTER;
793 doAnswer(invocation -> {
795 handler.onValueReceived("LastChange", value, "AVTransport");
797 return Collections.emptyMap();
798 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Pause"), anyMap());
801 private void expectLastChangeOnSetVolume(boolean respond, long volume) {
802 Map<String, String> inputs = new HashMap<>();
803 inputs.put("InstanceID", "0");
804 inputs.put("Channel", UPNP_MASTER);
805 inputs.put("DesiredVolume", String.valueOf(volume));
806 doAnswer(invocation -> {
808 handler.onValueReceived(UPNP_MASTER + "Volume", String.valueOf(volume), "RenderingControl");
810 return Collections.emptyMap();
811 }).when(upnpIOService).invokeAction(eq(handler), eq("RenderingControl"), eq("SetVolume"), eq(inputs));
814 private void expectLastChangeOnGetPositionInfo(boolean respond, String seekTarget) {
815 Map<String, String> inputs = new HashMap<>();
816 inputs.put("InstanceID", "0");
817 doAnswer(invocation -> {
819 handler.onValueReceived("RelTime", seekTarget, "AVTransport");
821 return Collections.emptyMap();
822 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("GetPositionInfo"), eq(inputs));
825 private void expectLastChangeOnSetAVTransportURI(boolean respond, int mediaId) {
826 expectLastChangeOnSetAVTransportURI(respond, true, mediaId);
829 private void expectLastChangeOnSetAVTransportURI(boolean respond, boolean withMetadata, int mediaId) {
830 String uri = upnpEntryQueue.get(mediaId).getRes();
831 String metadata = UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(mediaId)));
832 Map<String, String> inputs = new HashMap<>();
833 inputs.put("InstanceID", "0");
834 inputs.put("CurrentURI", uri);
835 inputs.put("CurrentURIMetaData", withMetadata ? metadata : "");
836 String value = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + uri + CLOSE + AV_TRANSPORT_URI_METADATA + metadata
837 + CLOSE + CURRENT_TRACK_URI + uri + CLOSE + CURRENT_TRACK_METADATA + metadata + CLOSE
838 + LAST_CHANGE_FOOTER;
839 doAnswer(invocation -> {
841 handler.onValueReceived("LastChange", value, "AVTransport");
843 return Collections.emptyMap();
844 }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("SetAVTransportURI"), eq(inputs));
847 private void checkInternalState(@Nullable Integer currentEntry, @Nullable Integer nextEntry, boolean playerStopped,
848 boolean playing, boolean registeredQueue, boolean playingQueue) {
849 if (currentEntry == null) {
850 assertNull(handler.currentEntry);
852 assertThat(handler.currentEntry, is(upnpEntryQueue.get(currentEntry)));
854 if (nextEntry == null) {
855 assertNull(handler.nextEntry);
857 assertThat(handler.nextEntry, is(upnpEntryQueue.get(nextEntry)));
859 assertThat(handler.playerStopped, is(playerStopped));
860 assertThat(handler.playing, is(playing));
861 assertThat(handler.registeredQueue, is(registeredQueue));
862 assertThat(handler.playingQueue, is(playingQueue));
865 private void checkControlChannel(Command command) {
866 ArgumentCaptor<PlayPauseType> captor = ArgumentCaptor.forClass(PlayPauseType.class);
867 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CONTROL).getUID()), captor.capture());
868 assertThat(captor.getValue(), is(command));
871 private void checkSetURI(@Nullable Integer current, @Nullable Integer next) {
872 checkSetURI(current, next, true);
875 private void checkSetURI(@Nullable Integer current, @Nullable Integer next, boolean withMetadata) {
876 ArgumentCaptor<String> uriCaptor = ArgumentCaptor.forClass(String.class);
877 ArgumentCaptor<String> metadataCaptor = ArgumentCaptor.forClass(String.class);
878 if (current != null) {
879 verify(handler, atLeastOnce()).setCurrentURI(uriCaptor.capture(), metadataCaptor.capture());
880 assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(current).getRes()));
882 assertThat(metadataCaptor.getValue(),
883 is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(current)))));
887 verify(handler, atLeastOnce()).setNextURI(uriCaptor.capture(), metadataCaptor.capture());
888 assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(next).getRes()));
890 assertThat(metadataCaptor.getValue(),
891 is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(next)))));
896 private void checkMetadataChannels(int mediaId) {
897 checkMetadataChannels(mediaId, false);
900 private void checkMetadataChannels(int mediaId, boolean cleared) {
901 ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
903 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(URI).getUID()), stateCaptor.capture());
904 assertThat(stateCaptor.getValue(), is(StringType.valueOf(upnpEntryQueue.get(mediaId).getRes())));
906 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TITLE).getUID()), stateCaptor.capture());
907 assertThat(stateCaptor.getValue(),
908 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getTitle())));
909 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ALBUM).getUID()), stateCaptor.capture());
910 assertThat(stateCaptor.getValue(),
911 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getAlbum())));
912 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CREATOR).getUID()), stateCaptor.capture());
913 assertThat(stateCaptor.getValue(),
914 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getCreator())));
915 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ARTIST).getUID()), stateCaptor.capture());
916 assertThat(stateCaptor.getValue(),
917 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getArtist())));
918 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(PUBLISHER).getUID()), stateCaptor.capture());
919 assertThat(stateCaptor.getValue(),
920 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getPublisher())));
921 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(GENRE).getUID()), stateCaptor.capture());
922 assertThat(stateCaptor.getValue(),
923 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getGenre())));
924 verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TRACK_NUMBER).getUID()),
925 stateCaptor.capture());
926 Integer originalTrackNumber = upnpEntryQueue.get(mediaId).getOriginalTrackNumber();
927 if (originalTrackNumber != null) {
928 assertThat(stateCaptor.getValue(), is(cleared ? UnDefType.UNDEF : new DecimalType(originalTrackNumber)));
929 is(new DecimalType(originalTrackNumber));