]> git.basschouten.com Git - openhab-addons.git/blob
985760c2060e333e8dcbf952a70e825a3441c330
[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.upnpcontrol.internal.handler;
14
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.*;
22
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.HashMap;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.concurrent.TimeUnit;
29
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;
62
63 /**
64  * Unit tests for {@link UpnpRendererHandler}.
65  *
66  * @author Mark Herwege - Initial contribution
67  */
68 @SuppressWarnings({ "null", "unchecked" })
69 @NonNullByDefault
70 public class UpnpRendererHandlerTest extends UpnpHandlerTest {
71
72     private final Logger logger = LoggerFactory.getLogger(UpnpRendererHandlerTest.class);
73
74     private static final String THING_TYPE_UID = "upnpcontrol:upnprenderer";
75     private static final String THING_UID = THING_TYPE_UID + ":mockrenderer";
76
77     private static final String LAST_CHANGE_HEADER = "<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/AVT/\">"
78             + "<InstanceID val=\"0\">";
79     private static final String LAST_CHANGE_FOOTER = "</InstanceID></Event>";
80     private static final String AV_TRANSPORT_URI = "<AVTransportURI val=\"";
81     private static final String AV_TRANSPORT_URI_METADATA = "<AVTransportURIMetaData val=\"";
82     private static final String CURRENT_TRACK_URI = "<CurrentTrackURI val=\"";
83     private static final String CURRENT_TRACK_METADATA = "<CurrentTrackMetaData val=\"";
84     private static final String TRANSPORT_STATE = "<TransportState val=\"";
85     private static final String CLOSE = "\"/>";
86
87     protected @Nullable UpnpRendererHandler handler;
88
89     private @Nullable UpnpEntryQueue upnpEntryQueue;
90
91     private ChannelUID volumeChannelUID = new ChannelUID(THING_UID + ":" + VOLUME);
92     private Channel volumeChannel = ChannelBuilder.create(volumeChannelUID, "Dimmer").build();
93
94     private ChannelUID muteChannelUID = new ChannelUID(THING_UID + ":" + MUTE);
95     private Channel muteChannel = ChannelBuilder.create(muteChannelUID, "Switch").build();
96
97     private ChannelUID stopChannelUID = new ChannelUID(THING_UID + ":" + STOP);
98     private Channel stopChannel = ChannelBuilder.create(stopChannelUID, "Switch").build();
99
100     private ChannelUID controlChannelUID = new ChannelUID(THING_UID + ":" + CONTROL);
101     private Channel controlChannel = ChannelBuilder.create(controlChannelUID, "Player").build();
102
103     private ChannelUID repeatChannelUID = new ChannelUID(THING_UID + ":" + REPEAT);
104     private Channel repeatChannel = ChannelBuilder.create(repeatChannelUID, "Switch").build();
105
106     private ChannelUID shuffleChannelUID = new ChannelUID(THING_UID + ":" + SHUFFLE);
107     private Channel shuffleChannel = ChannelBuilder.create(shuffleChannelUID, "Switch").build();
108
109     private ChannelUID onlyPlayOneChannelUID = new ChannelUID(THING_UID + ":" + ONLY_PLAY_ONE);
110     private Channel onlyPlayOneChannel = ChannelBuilder.create(onlyPlayOneChannelUID, "Switch").build();
111
112     private ChannelUID uriChannelUID = new ChannelUID(THING_UID + ":" + URI);
113     private Channel uriChannel = ChannelBuilder.create(uriChannelUID, "String").build();
114
115     private ChannelUID favoriteSelectChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_SELECT);
116     private Channel favoriteSelectChannel = ChannelBuilder.create(favoriteSelectChannelUID, "String").build();
117
118     private ChannelUID favoriteChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE);
119     private Channel favoriteChannel = ChannelBuilder.create(favoriteChannelUID, "String").build();
120
121     private ChannelUID favoriteActionChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_ACTION);
122     private Channel favoriteActionChannel = ChannelBuilder.create(favoriteActionChannelUID, "String").build();
123
124     private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
125     private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
126
127     private ChannelUID titleChannelUID = new ChannelUID(THING_UID + ":" + TITLE);
128     private Channel titleChannel = ChannelBuilder.create(titleChannelUID, "String").build();
129
130     private ChannelUID albumChannelUID = new ChannelUID(THING_UID + ":" + ALBUM);
131     private Channel albumChannel = ChannelBuilder.create(albumChannelUID, "String").build();
132
133     private ChannelUID albumArtChannelUID = new ChannelUID(THING_UID + ":" + ALBUM_ART);
134     private Channel albumArtChannel = ChannelBuilder.create(albumArtChannelUID, "Image").build();
135
136     private ChannelUID creatorChannelUID = new ChannelUID(THING_UID + ":" + CREATOR);
137     private Channel creatorChannel = ChannelBuilder.create(creatorChannelUID, "String").build();
138
139     private ChannelUID artistChannelUID = new ChannelUID(THING_UID + ":" + ARTIST);
140     private Channel artistChannel = ChannelBuilder.create(artistChannelUID, "String").build();
141
142     private ChannelUID publisherChannelUID = new ChannelUID(THING_UID + ":" + PUBLISHER);
143     private Channel publisherChannel = ChannelBuilder.create(publisherChannelUID, "String").build();
144
145     private ChannelUID genreChannelUID = new ChannelUID(THING_UID + ":" + GENRE);
146     private Channel genreChannel = ChannelBuilder.create(genreChannelUID, "String").build();
147
148     private ChannelUID trackNumberChannelUID = new ChannelUID(THING_UID + ":" + TRACK_NUMBER);
149     private Channel trackNumberChannel = ChannelBuilder.create(trackNumberChannelUID, "Number").build();
150
151     private ChannelUID trackDurationChannelUID = new ChannelUID(THING_UID + ":" + TRACK_DURATION);
152     private Channel trackDurationChannel = ChannelBuilder.create(trackDurationChannelUID, "Number:Time").build();
153
154     private ChannelUID trackPositionChannelUID = new ChannelUID(THING_UID + ":" + TRACK_POSITION);
155     private Channel trackPositionChannel = ChannelBuilder.create(trackPositionChannelUID, "Number:Time").build();
156
157     private ChannelUID relTrackPositionChannelUID = new ChannelUID(THING_UID + ":" + REL_TRACK_POSITION);
158     private Channel relTrackPositionChannel = ChannelBuilder.create(relTrackPositionChannelUID, "Dimmer").build();
159
160     @Mock
161     private @Nullable UpnpAudioSinkReg audioSinkReg;
162
163     @Override
164     @BeforeEach
165     public void setUp() {
166         super.setUp();
167
168         // stub thing methods
169         when(thing.getUID()).thenReturn(new ThingUID("upnpcontrol", "upnprenderer", "mockrenderer"));
170         when(thing.getLabel()).thenReturn("MockRenderer");
171         when(thing.getStatus()).thenReturn(ThingStatus.OFFLINE);
172
173         // stub channels
174         when(thing.getChannel(VOLUME)).thenReturn(volumeChannel);
175         when(thing.getChannel(MUTE)).thenReturn(muteChannel);
176         when(thing.getChannel(STOP)).thenReturn(stopChannel);
177         when(thing.getChannel(CONTROL)).thenReturn(controlChannel);
178         when(thing.getChannel(REPEAT)).thenReturn(repeatChannel);
179         when(thing.getChannel(SHUFFLE)).thenReturn(shuffleChannel);
180         when(thing.getChannel(ONLY_PLAY_ONE)).thenReturn(onlyPlayOneChannel);
181         when(thing.getChannel(URI)).thenReturn(uriChannel);
182         when(thing.getChannel(FAVORITE_SELECT)).thenReturn(favoriteSelectChannel);
183         when(thing.getChannel(FAVORITE)).thenReturn(favoriteChannel);
184         when(thing.getChannel(FAVORITE_ACTION)).thenReturn(favoriteActionChannel);
185         when(thing.getChannel(PLAYLIST_SELECT)).thenReturn(playlistSelectChannel);
186         when(thing.getChannel(TITLE)).thenReturn(titleChannel);
187         when(thing.getChannel(ALBUM)).thenReturn(albumChannel);
188         when(thing.getChannel(ALBUM_ART)).thenReturn(albumArtChannel);
189         when(thing.getChannel(CREATOR)).thenReturn(creatorChannel);
190         when(thing.getChannel(ARTIST)).thenReturn(artistChannel);
191         when(thing.getChannel(PUBLISHER)).thenReturn(publisherChannel);
192         when(thing.getChannel(GENRE)).thenReturn(genreChannel);
193         when(thing.getChannel(TRACK_NUMBER)).thenReturn(trackNumberChannel);
194         when(thing.getChannel(TRACK_DURATION)).thenReturn(trackDurationChannel);
195         when(thing.getChannel(TRACK_POSITION)).thenReturn(trackPositionChannel);
196         when(thing.getChannel(REL_TRACK_POSITION)).thenReturn(relTrackPositionChannel);
197
198         // stub config for initialize
199         when(config.as(UpnpControlRendererConfiguration.class)).thenReturn(new UpnpControlRendererConfiguration());
200
201         // create a media queue for playing
202         List<UpnpEntry> entries = createUpnpEntries();
203         upnpEntryQueue = new UpnpEntryQueue(entries, "54321");
204
205         handler = spy(new UpnpRendererHandler(requireNonNull(thing), requireNonNull(upnpIOService),
206                 requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider),
207                 requireNonNull(upnpCommandDescriptionProvider), configuration));
208
209         initHandler(requireNonNull(handler));
210
211         handler.initialize();
212
213         expectLastChangeOnStop(true);
214         expectLastChangeOnPlay(true);
215         expectLastChangeOnPause(true);
216     }
217
218     private List<UpnpEntry> createUpnpEntries() {
219         List<UpnpEntry> entries = new ArrayList<>();
220         UpnpEntry entry;
221         List<UpnpEntryRes> resList;
222         UpnpEntryRes res;
223         resList = new ArrayList<>();
224         res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 8054458L, "10", "http://MediaServerContent_0/1/M0/");
225         res.setRes("http://MediaServerContent_0/1/M0/Test_0.mp3");
226         resList.add(res);
227         entry = new UpnpEntry("M0", "M0", "C11", "object.item.audioItem").withTitle("Music_00").withResList(resList)
228                 .withAlbum("My Music 0").withCreator("Creator_0").withArtist("Artist_0").withGenre("Morning")
229                 .withPublisher("myself 0").withAlbumArtUri("").withTrackNumber(1);
230         entries.add(entry);
231         resList = new ArrayList<>();
232         res = new UpnpEntryRes("http-get:*:audio/wav:*", 1156598L, "6", "http://MediaServerContent_0/1/M1/");
233         res.setRes("http://MediaServerContent_0/1/M1/Test_1.wav");
234         resList.add(res);
235         entry = new UpnpEntry("M1", "M1", "C11", "object.item.audioItem").withTitle("Music_01").withResList(resList)
236                 .withAlbum("My Music 0").withCreator("Creator_1").withArtist("Artist_1").withGenre("Morning")
237                 .withPublisher("myself 1").withAlbumArtUri("").withTrackNumber(2);
238         entries.add(entry);
239         resList = new ArrayList<>();
240         res = new UpnpEntryRes("http-get:*:audio/mpeg:*", 1156598L, "40", "http://MediaServerContent_0/1/M2/");
241         res.setRes("http://MediaServerContent_0/1/M2/Test_2.mp3");
242         resList.add(res);
243         entry = new UpnpEntry("M2", "M2", "C12", "object.item.audioItem").withTitle("Music_02").withResList(resList)
244                 .withAlbum("My Music 2").withCreator("Creator_2").withArtist("Artist_2").withGenre("Evening")
245                 .withPublisher("myself 2").withAlbumArtUri("").withTrackNumber(1);
246         entries.add(entry);
247         return entries;
248     }
249
250     @Override
251     @AfterEach
252     public void tearDown() {
253         handler.dispose();
254
255         super.tearDown();
256     }
257
258     @Test
259     public void testRegisterQueue() {
260         logger.info("testRegisterQueue");
261
262         // Register a media queue
263         expectLastChangeOnSetAVTransportURI(true, 0);
264         handler.registerQueue(requireNonNull(upnpEntryQueue));
265
266         checkInternalState(0, 1, true, false, true, false);
267         checkControlChannel(PlayPauseType.PAUSE);
268         checkSetURI(0, 1);
269         checkMetadataChannels(0);
270     }
271
272     @Test
273     public void testPlayQueue() {
274         logger.info("testPlayQueue");
275
276         // Register a media queue
277         expectLastChangeOnSetAVTransportURI(true, 0);
278         handler.registerQueue(requireNonNull(upnpEntryQueue));
279
280         // Play media
281         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
282
283         checkInternalState(0, 1, false, true, false, true);
284         checkControlChannel(PlayPauseType.PLAY);
285         checkSetURI(0, 1);
286         checkMetadataChannels(0);
287     }
288
289     @Test
290     public void testStop() {
291         logger.info("testStop");
292
293         // Register a media queue
294         expectLastChangeOnSetAVTransportURI(true, 0);
295         handler.registerQueue(requireNonNull(upnpEntryQueue));
296
297         // Play media
298         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
299
300         // Stop playback
301         handler.handleCommand(stopChannelUID, OnOffType.ON);
302
303         checkInternalState(0, 1, true, false, false, false);
304         checkControlChannel(PlayPauseType.PAUSE);
305         checkSetURI(0, 1);
306         checkMetadataChannels(0);
307     }
308
309     @Test
310     public void testPause() {
311         logger.info("testPause");
312
313         // Register a media queue
314         expectLastChangeOnSetAVTransportURI(true, 0);
315         handler.registerQueue(requireNonNull(upnpEntryQueue));
316
317         // Play media
318         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
319
320         // Pause media
321         handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
322
323         checkControlChannel(PlayPauseType.PAUSE);
324
325         // Continue playing
326         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
327
328         checkControlChannel(PlayPauseType.PLAY);
329     }
330
331     @Test
332     public void testPauseNotSupported() {
333         logger.info("testPauseNotSupported");
334
335         // Some players don't support pause and just continue playing.
336         // Test if we properly switch back to playing state if no confirmation of pause received.
337
338         // Register a media queue
339         expectLastChangeOnSetAVTransportURI(true, 0);
340         handler.registerQueue(requireNonNull(upnpEntryQueue));
341
342         // Play media
343         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
344
345         // Pause media
346         // Do not receive a PAUSED_PLAYBACK response
347         expectLastChangeOnPause(false);
348         handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
349
350         // Wait long enough for status to turn back to PLAYING.
351         // All timeouts in test are set to 1s.
352         try {
353             TimeUnit.SECONDS.sleep(1);
354         } catch (InterruptedException ignore) {
355         }
356
357         checkControlChannel(PlayPauseType.PLAY);
358     }
359
360     @Test
361     public void testRegisterQueueWhilePlaying() {
362         logger.info("testRegisterQueueWhilePlaying");
363
364         // Register a media queue
365         expectLastChangeOnSetAVTransportURI(true, 2);
366         List<UpnpEntry> startList = new ArrayList<UpnpEntry>();
367         startList.add(requireNonNull(upnpEntryQueue.get(2)));
368         UpnpEntryQueue startQueue = new UpnpEntryQueue(startList, "54321");
369         handler.registerQueue(requireNonNull(startQueue));
370
371         // Play media
372         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
373
374         // Register a new media queue
375         expectLastChangeOnSetAVTransportURI(true, 0);
376         handler.registerQueue(requireNonNull(upnpEntryQueue));
377
378         checkInternalState(2, 0, false, true, true, true);
379         checkControlChannel(PlayPauseType.PLAY);
380         checkSetURI(null, 0);
381         checkMetadataChannels(2);
382     }
383
384     @Test
385     public void testNext() {
386         logger.info("testNext");
387
388         testNext(false, false);
389     }
390
391     @Test
392     public void testNextRepeat() {
393         logger.info("testNextRepeat");
394
395         testNext(false, true);
396     }
397
398     @Test
399     public void testNextWhilePlaying() {
400         logger.info("testNextWhilePlaying");
401
402         testNext(true, false);
403     }
404
405     @Test
406     public void testNextWhilePlayingRepeat() {
407         logger.info("testNextWhilePlayingRepeat");
408
409         testNext(true, true);
410     }
411
412     private void testNext(boolean play, boolean repeat) {
413         // Register a media queue
414         expectLastChangeOnSetAVTransportURI(true, 0);
415         handler.registerQueue(requireNonNull(upnpEntryQueue));
416
417         if (repeat) {
418             handler.handleCommand(repeatChannelUID, OnOffType.ON);
419         }
420
421         if (play) {
422             // Play media
423             handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
424         }
425
426         // Next media
427         expectLastChangeOnSetAVTransportURI(true, 1);
428         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
429
430         checkInternalState(1, 2, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
431         checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
432         checkSetURI(1, 2);
433         checkMetadataChannels(1);
434
435         // Next media
436         expectLastChangeOnSetAVTransportURI(true, 2);
437         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
438
439         checkInternalState(2, repeat ? 0 : null, play ? false : true, play ? true : false, play ? false : true,
440                 play ? true : false);
441         checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
442         checkSetURI(2, repeat ? 0 : null);
443         checkMetadataChannels(2);
444
445         // Next media
446         expectLastChangeOnSetAVTransportURI(true, 0);
447         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
448
449         checkInternalState(0, 1, (play && repeat) ? false : true, (play && repeat) ? true : false,
450                 (play && repeat) ? false : true, (play && repeat) ? true : false);
451         checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
452         checkSetURI(0, 1);
453         checkMetadataChannels(0);
454     }
455
456     @Test
457     public void testPrevious() {
458         logger.info("testPrevious");
459
460         testPrevious(false, false);
461     }
462
463     @Test
464     public void testPreviousRepeat() {
465         logger.info("testPreviousRepeat");
466
467         testPrevious(false, true);
468     }
469
470     @Test
471     public void testPreviousWhilePlaying() {
472         logger.info("testPreviousWhilePlaying");
473
474         testPrevious(true, false);
475     }
476
477     @Test
478     public void testPreviousWhilePlayingRepeat() {
479         logger.info("testPreviousWhilePlayingRepeat");
480
481         testPrevious(true, true);
482     }
483
484     public void testPrevious(boolean play, boolean repeat) {
485         // Register a media queue
486         expectLastChangeOnSetAVTransportURI(true, 0);
487         handler.registerQueue(requireNonNull(upnpEntryQueue));
488
489         if (repeat) {
490             handler.handleCommand(repeatChannelUID, OnOffType.ON);
491         }
492
493         if (play) {
494             // Play media
495             handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
496         }
497
498         // Next media
499         expectLastChangeOnSetAVTransportURI(true, 1);
500         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
501
502         // Previous media
503         expectLastChangeOnSetAVTransportURI(true, 2);
504         handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
505
506         checkInternalState(0, 1, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
507         checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
508         checkSetURI(0, 1);
509         checkMetadataChannels(0);
510
511         // Previous media
512         expectLastChangeOnSetAVTransportURI(true, 0);
513         handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
514
515         checkInternalState(repeat ? 2 : 0, repeat ? 0 : 1, (play && repeat) ? false : true,
516                 (play && repeat) ? true : false, (play && repeat) ? false : true, (play && repeat) ? true : false);
517         checkControlChannel((play && repeat) ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
518         checkSetURI(repeat ? 2 : 0, repeat ? 0 : 1);
519         checkMetadataChannels(repeat ? 2 : 0);
520     }
521
522     @Test
523     public void testAutoPlayNextInQueue() {
524         logger.info("testAutoPlayNextInQueue");
525
526         // Register a media queue
527         expectLastChangeOnSetAVTransportURI(true, 0);
528         handler.registerQueue(requireNonNull(upnpEntryQueue));
529
530         // Play media
531         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
532
533         // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
534         expectLastChangeOnSetAVTransportURI(true, 1);
535
536         // At the end of the media, we will get GENA LastChange STOP event, renderer should move to next media and play
537         // Force this STOP event for test
538         String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
539         handler.onValueReceived("LastChange", lastChange, "AVTransport");
540
541         checkInternalState(1, 2, false, true, false, true);
542         checkControlChannel(PlayPauseType.PLAY);
543         checkSetURI(1, 2);
544         checkMetadataChannels(1);
545     }
546
547     @Test
548     public void testAutoPlayNextInQueueGapless() {
549         logger.info("testAutoPlayNextInQueueGapless");
550
551         // Register a media queue
552         expectLastChangeOnSetAVTransportURI(true, 0);
553         handler.registerQueue(requireNonNull(upnpEntryQueue));
554
555         // Play media
556         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
557
558         // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
559         expectLastChangeOnSetAVTransportURI(true, 1);
560
561         // At the end of the media, we will get GENA event with new URI and metadata
562         String lastChange = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + upnpEntryQueue.get(1).getRes() + CLOSE
563                 + AV_TRANSPORT_URI_METADATA + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(0)))
564                 + CLOSE + CURRENT_TRACK_URI + upnpEntryQueue.get(1).getRes() + CLOSE + CURRENT_TRACK_METADATA
565                 + UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(1))) + CLOSE
566                 + LAST_CHANGE_FOOTER;
567         handler.onValueReceived("LastChange", lastChange, "AVTransport");
568
569         checkInternalState(1, 2, false, true, false, true);
570         checkControlChannel(PlayPauseType.PLAY);
571         checkSetURI(null, 2);
572         checkMetadataChannels(1);
573     }
574
575     @Test
576     public void testOnlyPlayOne() {
577         logger.info("testOnlyPlayOne");
578
579         handler.handleCommand(onlyPlayOneChannelUID, OnOffType.ON);
580
581         // Register a media queue
582         expectLastChangeOnSetAVTransportURI(true, 0);
583         handler.registerQueue(requireNonNull(upnpEntryQueue));
584
585         // Play media
586         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
587
588         checkInternalState(0, 1, false, true, false, true);
589         checkSetURI(0, null);
590         checkMetadataChannels(0);
591
592         // We expect GENA LastChange event with new metadata when the renderer has finished playing
593         expectLastChangeOnSetAVTransportURI(true, 1);
594
595         // At the end of the media, we will get GENA LastChange STOP event, renderer should stop
596         // Force this STOP event for test
597         String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
598         handler.onValueReceived("LastChange", lastChange, "AVTransport");
599
600         checkInternalState(1, 2, false, false, false, true);
601         checkControlChannel(PlayPauseType.PAUSE);
602         checkSetURI(1, null);
603         checkMetadataChannels(1);
604     }
605
606     @Test
607     public void testPlayUri() {
608         logger.info("testPlayUri");
609
610         expectLastChangeOnSetAVTransportURI(true, false, 0);
611         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
612
613         // Play media
614         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
615
616         checkInternalState(null, null, false, true, false, false);
617         checkControlChannel(PlayPauseType.PLAY);
618         checkSetURI(0, null, false);
619         checkMetadataChannels(0, true);
620     }
621
622     @Test
623     public void testPlayAction() {
624         logger.info("testPlayAction");
625
626         expectLastChangeOnSetAVTransportURI(true, false, 0);
627
628         // Methods called in sequence by audio sink
629         handler.setCurrentURI(upnpEntryQueue.get(0).getRes(), "");
630         handler.play();
631
632         checkInternalState(null, null, false, true, false, false);
633         checkControlChannel(PlayPauseType.PLAY);
634         checkSetURI(0, null, false);
635         checkMetadataChannels(0, true);
636     }
637
638     @Test
639     public void testPlayNotification() {
640         logger.info("testPlayNotification");
641
642         // Register a media queue
643         expectLastChangeOnSetAVTransportURI(true, 0);
644         handler.registerQueue(requireNonNull(upnpEntryQueue));
645
646         // Set volume
647         expectLastChangeOnSetVolume(true, 50);
648         handler.setVolume(new PercentType(50));
649
650         checkInternalState(0, 1, true, false, true, false);
651         checkSetURI(0, 1, true);
652         checkMetadataChannels(0, false);
653
654         // Play notification, at standard 10% volume above current volume level
655         expectLastChangeOnSetAVTransportURI(true, false, 2);
656         expectLastChangeOnGetPositionInfo(true, "00:00:00");
657         handler.playNotification(upnpEntryQueue.get(2).getRes());
658
659         checkInternalState(0, 1, true, false, true, false);
660         checkSetURI(2, null, false);
661         checkMetadataChannels(0, false);
662         verify(handler).setVolume(new PercentType(55));
663
664         // At the end of the notification, we will get GENA LastChange STOP event
665         // Force this STOP event for test
666         expectLastChangeOnSetAVTransportURI(true, false, 0);
667         String lastChange = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
668         handler.onValueReceived("LastChange", lastChange, "AVTransport");
669
670         checkInternalState(0, 1, true, false, true, false);
671         checkMetadataChannels(0, false);
672         verify(handler, times(2)).setVolume(new PercentType(50));
673
674         // Play media and move to position
675         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
676
677         checkInternalState(0, 1, false, true, false, true); //
678         checkSetURI(0, 1, true);
679         checkMetadataChannels(0, false);
680
681         // Play notification again, while simulating the current playing media is at 10s position
682         // Play at volume level provided by audiSink action
683         expectLastChangeOnSetAVTransportURI(true, false, 2);
684         expectLastChangeOnGetPositionInfo(true, "00:00:10");
685         handler.setNotificationVolume(new PercentType(70));
686         handler.playNotification(upnpEntryQueue.get(2).getRes());
687
688         checkInternalState(0, 1, false, true, false, true);
689         checkSetURI(2, null, false);
690         checkMetadataChannels(0, false);
691         verify(handler).setVolume(new PercentType(70));
692
693         // Wait long enough for max notification duration to be reached.
694         // In the test, we have enforced 500ms delay through schedule mock.
695         expectLastChangeOnSetAVTransportURI(true, false, 0);
696         try {
697             TimeUnit.SECONDS.sleep(1);
698             logger.info("Test playing {}, stopped {}", handler.playing, handler.playerStopped);
699         } catch (InterruptedException ignore) {
700         }
701
702         checkInternalState(0, 1, false, true, false, true);
703         checkSetURI(0, null, false);
704         checkMetadataChannels(0, false);
705         verify(handler, times(3)).setVolume(new PercentType(50));
706         verify(callback, times(2)).stateUpdated(trackPositionChannelUID, new QuantityType<>(10, Units.SECOND));
707     }
708
709     @Test
710     public void testFavorite() {
711         logger.info("testFavorite");
712
713         // Check already called in initialize
714         verify(handler).updateFavoritesList();
715
716         // First set URI
717         expectLastChangeOnSetAVTransportURI(true, false, 0);
718         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
719
720         // Save favorite
721         handler.handleCommand(favoriteChannelUID, StringType.valueOf("Test_Favorite"));
722         handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("SAVE"));
723
724         // Check called after saving favorite
725         verify(handler, times(2)).updateFavoritesList();
726
727         // Check that FAVORITE_SELECT channel now has the favorite as a state option
728         ArgumentCaptor<List<CommandOption>> commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
729         verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
730                 commandOptionListCaptor.capture());
731         assertThat(commandOptionListCaptor.getValue().size(), is(1));
732         assertThat(commandOptionListCaptor.getValue().get(0).getCommand(), is("Test_Favorite"));
733         assertThat(commandOptionListCaptor.getValue().get(0).getLabel(), is("Test_Favorite"));
734
735         // Clear FAVORITE channel
736         handler.handleCommand(favoriteChannelUID, StringType.valueOf(""));
737
738         // Set another URI
739         expectLastChangeOnSetAVTransportURI(true, false, 2);
740         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(2).getRes()));
741
742         checkInternalState(null, null, false, true, false, false);
743         checkSetURI(2, null, false);
744         checkMetadataChannels(2, true);
745
746         // Restore favorite
747         expectLastChangeOnSetAVTransportURI(true, false, 0);
748         handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
749
750         checkInternalState(null, null, false, true, false, false);
751         checkControlChannel(PlayPauseType.PLAY);
752         checkSetURI(0, null, false);
753         checkMetadataChannels(0, true);
754
755         // Delete favorite
756         handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
757         handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("DELETE"));
758
759         // Check called after deleting favorite
760         verify(handler, times(3)).updateFavoritesList();
761
762         // Check that FAVORITE_SELECT channel option list is empty again
763         commandOptionListCaptor = ArgumentCaptor.forClass(List.class);
764         verify(handler, atLeastOnce()).updateCommandDescription(eq(thing.getChannel(FAVORITE_SELECT).getUID()),
765                 commandOptionListCaptor.capture());
766         assertThat(commandOptionListCaptor.getValue().size(), is(0));
767     }
768
769     private void expectLastChangeOnStop(boolean respond) {
770         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
771         doAnswer(invocation -> {
772             if (respond) {
773                 handler.onValueReceived("LastChange", value, "AVTransport");
774             }
775             return Collections.emptyMap();
776         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Stop"), anyMap());
777     }
778
779     private void expectLastChangeOnPlay(boolean respond) {
780         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PLAYING" + CLOSE + LAST_CHANGE_FOOTER;
781         doAnswer(invocation -> {
782             if (respond) {
783                 handler.onValueReceived("LastChange", value, "AVTransport");
784             }
785             return Collections.emptyMap();
786         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Play"), anyMap());
787     }
788
789     private void expectLastChangeOnPause(boolean respond) {
790         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PAUSED_PLAYBACK" + CLOSE + LAST_CHANGE_FOOTER;
791         doAnswer(invocation -> {
792             if (respond) {
793                 handler.onValueReceived("LastChange", value, "AVTransport");
794             }
795             return Collections.emptyMap();
796         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Pause"), anyMap());
797     }
798
799     private void expectLastChangeOnSetVolume(boolean respond, long volume) {
800         Map<String, String> inputs = new HashMap<>();
801         inputs.put("InstanceID", "0");
802         inputs.put("Channel", UPNP_MASTER);
803         inputs.put("DesiredVolume", String.valueOf(volume));
804         doAnswer(invocation -> {
805             if (respond) {
806                 handler.onValueReceived(UPNP_MASTER + "Volume", String.valueOf(volume), "RenderingControl");
807             }
808             return Collections.emptyMap();
809         }).when(upnpIOService).invokeAction(eq(handler), eq("RenderingControl"), eq("SetVolume"), eq(inputs));
810     }
811
812     private void expectLastChangeOnGetPositionInfo(boolean respond, String seekTarget) {
813         Map<String, String> inputs = new HashMap<>();
814         inputs.put("InstanceID", "0");
815         doAnswer(invocation -> {
816             if (respond) {
817                 handler.onValueReceived("RelTime", seekTarget, "AVTransport");
818             }
819             return Collections.emptyMap();
820         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("GetPositionInfo"), eq(inputs));
821     }
822
823     private void expectLastChangeOnSetAVTransportURI(boolean respond, int mediaId) {
824         expectLastChangeOnSetAVTransportURI(respond, true, mediaId);
825     }
826
827     private void expectLastChangeOnSetAVTransportURI(boolean respond, boolean withMetadata, int mediaId) {
828         String uri = upnpEntryQueue.get(mediaId).getRes();
829         String metadata = UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(mediaId)));
830         Map<String, String> inputs = new HashMap<>();
831         inputs.put("InstanceID", "0");
832         inputs.put("CurrentURI", uri);
833         inputs.put("CurrentURIMetaData", withMetadata ? metadata : "");
834         String value = LAST_CHANGE_HEADER + AV_TRANSPORT_URI + uri + CLOSE + AV_TRANSPORT_URI_METADATA + metadata
835                 + CLOSE + CURRENT_TRACK_URI + uri + CLOSE + CURRENT_TRACK_METADATA + metadata + CLOSE
836                 + LAST_CHANGE_FOOTER;
837         doAnswer(invocation -> {
838             if (respond) {
839                 handler.onValueReceived("LastChange", value, "AVTransport");
840             }
841             return Collections.emptyMap();
842         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("SetAVTransportURI"), eq(inputs));
843     }
844
845     private void checkInternalState(@Nullable Integer currentEntry, @Nullable Integer nextEntry, boolean playerStopped,
846             boolean playing, boolean registeredQueue, boolean playingQueue) {
847         if (currentEntry == null) {
848             assertNull(handler.currentEntry);
849         } else {
850             assertThat(handler.currentEntry, is(upnpEntryQueue.get(currentEntry)));
851         }
852         if (nextEntry == null) {
853             assertNull(handler.nextEntry);
854         } else {
855             assertThat(handler.nextEntry, is(upnpEntryQueue.get(nextEntry)));
856         }
857         assertThat(handler.playerStopped, is(playerStopped));
858         assertThat(handler.playing, is(playing));
859         assertThat(handler.registeredQueue, is(registeredQueue));
860         assertThat(handler.playingQueue, is(playingQueue));
861     }
862
863     private void checkControlChannel(Command command) {
864         ArgumentCaptor<PlayPauseType> captor = ArgumentCaptor.forClass(PlayPauseType.class);
865         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CONTROL).getUID()), captor.capture());
866         assertThat(captor.getValue(), is(command));
867     }
868
869     private void checkSetURI(@Nullable Integer current, @Nullable Integer next) {
870         checkSetURI(current, next, true);
871     }
872
873     private void checkSetURI(@Nullable Integer current, @Nullable Integer next, boolean withMetadata) {
874         ArgumentCaptor<String> uriCaptor = ArgumentCaptor.forClass(String.class);
875         ArgumentCaptor<String> metadataCaptor = ArgumentCaptor.forClass(String.class);
876         if (current != null) {
877             verify(handler, atLeastOnce()).setCurrentURI(uriCaptor.capture(), metadataCaptor.capture());
878             assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(current).getRes()));
879             if (withMetadata) {
880                 assertThat(metadataCaptor.getValue(),
881                         is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(current)))));
882             }
883         }
884         if (next != null) {
885             verify(handler, atLeastOnce()).setNextURI(uriCaptor.capture(), metadataCaptor.capture());
886             assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(next).getRes()));
887             if (withMetadata) {
888                 assertThat(metadataCaptor.getValue(),
889                         is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(next)))));
890             }
891         }
892     }
893
894     private void checkMetadataChannels(int mediaId) {
895         checkMetadataChannels(mediaId, false);
896     }
897
898     private void checkMetadataChannels(int mediaId, boolean cleared) {
899         ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
900
901         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(URI).getUID()), stateCaptor.capture());
902         assertThat(stateCaptor.getValue(), is(StringType.valueOf(upnpEntryQueue.get(mediaId).getRes())));
903
904         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TITLE).getUID()), stateCaptor.capture());
905         assertThat(stateCaptor.getValue(),
906                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getTitle())));
907         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ALBUM).getUID()), stateCaptor.capture());
908         assertThat(stateCaptor.getValue(),
909                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getAlbum())));
910         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(CREATOR).getUID()), stateCaptor.capture());
911         assertThat(stateCaptor.getValue(),
912                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getCreator())));
913         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(ARTIST).getUID()), stateCaptor.capture());
914         assertThat(stateCaptor.getValue(),
915                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getArtist())));
916         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(PUBLISHER).getUID()), stateCaptor.capture());
917         assertThat(stateCaptor.getValue(),
918                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getPublisher())));
919         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(GENRE).getUID()), stateCaptor.capture());
920         assertThat(stateCaptor.getValue(),
921                 is(cleared ? UnDefType.UNDEF : StringType.valueOf(upnpEntryQueue.get(mediaId).getGenre())));
922         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(TRACK_NUMBER).getUID()),
923                 stateCaptor.capture());
924         Integer originalTrackNumber = upnpEntryQueue.get(mediaId).getOriginalTrackNumber();
925         if (originalTrackNumber != null) {
926             assertThat(stateCaptor.getValue(), is(cleared ? UnDefType.UNDEF : new DecimalType(originalTrackNumber)));
927             is(new DecimalType(originalTrackNumber));
928         }
929     }
930 }