]> git.basschouten.com Git - openhab-addons.git/blob
8d8de3526e0629732ff665056a7c8f10582708f9
[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 = """
78             <Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">\
79             <InstanceID val="0">\
80             """;
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 = "\"/>";
88
89     protected @Nullable UpnpRendererHandler handler;
90
91     private @Nullable UpnpEntryQueue upnpEntryQueue;
92
93     private ChannelUID volumeChannelUID = new ChannelUID(THING_UID + ":" + VOLUME);
94     private Channel volumeChannel = ChannelBuilder.create(volumeChannelUID, "Dimmer").build();
95
96     private ChannelUID muteChannelUID = new ChannelUID(THING_UID + ":" + MUTE);
97     private Channel muteChannel = ChannelBuilder.create(muteChannelUID, "Switch").build();
98
99     private ChannelUID stopChannelUID = new ChannelUID(THING_UID + ":" + STOP);
100     private Channel stopChannel = ChannelBuilder.create(stopChannelUID, "Switch").build();
101
102     private ChannelUID controlChannelUID = new ChannelUID(THING_UID + ":" + CONTROL);
103     private Channel controlChannel = ChannelBuilder.create(controlChannelUID, "Player").build();
104
105     private ChannelUID repeatChannelUID = new ChannelUID(THING_UID + ":" + REPEAT);
106     private Channel repeatChannel = ChannelBuilder.create(repeatChannelUID, "Switch").build();
107
108     private ChannelUID shuffleChannelUID = new ChannelUID(THING_UID + ":" + SHUFFLE);
109     private Channel shuffleChannel = ChannelBuilder.create(shuffleChannelUID, "Switch").build();
110
111     private ChannelUID onlyPlayOneChannelUID = new ChannelUID(THING_UID + ":" + ONLY_PLAY_ONE);
112     private Channel onlyPlayOneChannel = ChannelBuilder.create(onlyPlayOneChannelUID, "Switch").build();
113
114     private ChannelUID uriChannelUID = new ChannelUID(THING_UID + ":" + URI);
115     private Channel uriChannel = ChannelBuilder.create(uriChannelUID, "String").build();
116
117     private ChannelUID favoriteSelectChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_SELECT);
118     private Channel favoriteSelectChannel = ChannelBuilder.create(favoriteSelectChannelUID, "String").build();
119
120     private ChannelUID favoriteChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE);
121     private Channel favoriteChannel = ChannelBuilder.create(favoriteChannelUID, "String").build();
122
123     private ChannelUID favoriteActionChannelUID = new ChannelUID(THING_UID + ":" + FAVORITE_ACTION);
124     private Channel favoriteActionChannel = ChannelBuilder.create(favoriteActionChannelUID, "String").build();
125
126     private ChannelUID playlistSelectChannelUID = new ChannelUID(THING_UID + ":" + PLAYLIST_SELECT);
127     private Channel playlistSelectChannel = ChannelBuilder.create(playlistSelectChannelUID, "String").build();
128
129     private ChannelUID titleChannelUID = new ChannelUID(THING_UID + ":" + TITLE);
130     private Channel titleChannel = ChannelBuilder.create(titleChannelUID, "String").build();
131
132     private ChannelUID albumChannelUID = new ChannelUID(THING_UID + ":" + ALBUM);
133     private Channel albumChannel = ChannelBuilder.create(albumChannelUID, "String").build();
134
135     private ChannelUID albumArtChannelUID = new ChannelUID(THING_UID + ":" + ALBUM_ART);
136     private Channel albumArtChannel = ChannelBuilder.create(albumArtChannelUID, "Image").build();
137
138     private ChannelUID creatorChannelUID = new ChannelUID(THING_UID + ":" + CREATOR);
139     private Channel creatorChannel = ChannelBuilder.create(creatorChannelUID, "String").build();
140
141     private ChannelUID artistChannelUID = new ChannelUID(THING_UID + ":" + ARTIST);
142     private Channel artistChannel = ChannelBuilder.create(artistChannelUID, "String").build();
143
144     private ChannelUID publisherChannelUID = new ChannelUID(THING_UID + ":" + PUBLISHER);
145     private Channel publisherChannel = ChannelBuilder.create(publisherChannelUID, "String").build();
146
147     private ChannelUID genreChannelUID = new ChannelUID(THING_UID + ":" + GENRE);
148     private Channel genreChannel = ChannelBuilder.create(genreChannelUID, "String").build();
149
150     private ChannelUID trackNumberChannelUID = new ChannelUID(THING_UID + ":" + TRACK_NUMBER);
151     private Channel trackNumberChannel = ChannelBuilder.create(trackNumberChannelUID, "Number").build();
152
153     private ChannelUID trackDurationChannelUID = new ChannelUID(THING_UID + ":" + TRACK_DURATION);
154     private Channel trackDurationChannel = ChannelBuilder.create(trackDurationChannelUID, "Number:Time").build();
155
156     private ChannelUID trackPositionChannelUID = new ChannelUID(THING_UID + ":" + TRACK_POSITION);
157     private Channel trackPositionChannel = ChannelBuilder.create(trackPositionChannelUID, "Number:Time").build();
158
159     private ChannelUID relTrackPositionChannelUID = new ChannelUID(THING_UID + ":" + REL_TRACK_POSITION);
160     private Channel relTrackPositionChannel = ChannelBuilder.create(relTrackPositionChannelUID, "Dimmer").build();
161
162     @Mock
163     private @Nullable UpnpAudioSinkReg audioSinkReg;
164
165     @Override
166     @BeforeEach
167     public void setUp() {
168         super.setUp();
169
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);
174
175         // stub channels
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);
199
200         // stub config for initialize
201         when(config.as(UpnpControlRendererConfiguration.class)).thenReturn(new UpnpControlRendererConfiguration());
202
203         // create a media queue for playing
204         List<UpnpEntry> entries = createUpnpEntries();
205         upnpEntryQueue = new UpnpEntryQueue(entries, "54321");
206
207         handler = spy(new UpnpRendererHandler(requireNonNull(thing), requireNonNull(upnpIOService),
208                 requireNonNull(audioSinkReg), requireNonNull(upnpStateDescriptionProvider),
209                 requireNonNull(upnpCommandDescriptionProvider), configuration));
210
211         initHandler(requireNonNull(handler));
212
213         handler.initialize();
214
215         expectLastChangeOnStop(true);
216         expectLastChangeOnPlay(true);
217         expectLastChangeOnPause(true);
218     }
219
220     private List<UpnpEntry> createUpnpEntries() {
221         List<UpnpEntry> entries = new ArrayList<>();
222         UpnpEntry entry;
223         List<UpnpEntryRes> resList;
224         UpnpEntryRes res;
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");
228         resList.add(res);
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);
232         entries.add(entry);
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");
236         resList.add(res);
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);
240         entries.add(entry);
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");
244         resList.add(res);
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);
248         entries.add(entry);
249         return entries;
250     }
251
252     @Override
253     @AfterEach
254     public void tearDown() {
255         handler.dispose();
256
257         super.tearDown();
258     }
259
260     @Test
261     public void testRegisterQueue() {
262         logger.info("testRegisterQueue");
263
264         // Register a media queue
265         expectLastChangeOnSetAVTransportURI(true, 0);
266         handler.registerQueue(requireNonNull(upnpEntryQueue));
267
268         checkInternalState(0, 1, true, false, true, false);
269         checkControlChannel(PlayPauseType.PAUSE);
270         checkSetURI(0, 1);
271         checkMetadataChannels(0);
272     }
273
274     @Test
275     public void testPlayQueue() {
276         logger.info("testPlayQueue");
277
278         // Register a media queue
279         expectLastChangeOnSetAVTransportURI(true, 0);
280         handler.registerQueue(requireNonNull(upnpEntryQueue));
281
282         // Play media
283         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
284
285         checkInternalState(0, 1, false, true, false, true);
286         checkControlChannel(PlayPauseType.PLAY);
287         checkSetURI(0, 1);
288         checkMetadataChannels(0);
289     }
290
291     @Test
292     public void testStop() {
293         logger.info("testStop");
294
295         // Register a media queue
296         expectLastChangeOnSetAVTransportURI(true, 0);
297         handler.registerQueue(requireNonNull(upnpEntryQueue));
298
299         // Play media
300         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
301
302         // Stop playback
303         handler.handleCommand(stopChannelUID, OnOffType.ON);
304
305         checkInternalState(0, 1, true, false, false, false);
306         checkControlChannel(PlayPauseType.PAUSE);
307         checkSetURI(0, 1);
308         checkMetadataChannels(0);
309     }
310
311     @Test
312     public void testPause() {
313         logger.info("testPause");
314
315         // Register a media queue
316         expectLastChangeOnSetAVTransportURI(true, 0);
317         handler.registerQueue(requireNonNull(upnpEntryQueue));
318
319         // Play media
320         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
321
322         // Pause media
323         handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
324
325         checkControlChannel(PlayPauseType.PAUSE);
326
327         // Continue playing
328         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
329
330         checkControlChannel(PlayPauseType.PLAY);
331     }
332
333     @Test
334     public void testPauseNotSupported() {
335         logger.info("testPauseNotSupported");
336
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.
339
340         // Register a media queue
341         expectLastChangeOnSetAVTransportURI(true, 0);
342         handler.registerQueue(requireNonNull(upnpEntryQueue));
343
344         // Play media
345         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
346
347         // Pause media
348         // Do not receive a PAUSED_PLAYBACK response
349         expectLastChangeOnPause(false);
350         handler.handleCommand(controlChannelUID, PlayPauseType.PAUSE);
351
352         // Wait long enough for status to turn back to PLAYING.
353         // All timeouts in test are set to 1s.
354         try {
355             TimeUnit.SECONDS.sleep(1);
356         } catch (InterruptedException ignore) {
357         }
358
359         checkControlChannel(PlayPauseType.PLAY);
360     }
361
362     @Test
363     public void testRegisterQueueWhilePlaying() {
364         logger.info("testRegisterQueueWhilePlaying");
365
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));
372
373         // Play media
374         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
375
376         // Register a new media queue
377         expectLastChangeOnSetAVTransportURI(true, 0);
378         handler.registerQueue(requireNonNull(upnpEntryQueue));
379
380         checkInternalState(2, 0, false, true, true, true);
381         checkControlChannel(PlayPauseType.PLAY);
382         checkSetURI(null, 0);
383         checkMetadataChannels(2);
384     }
385
386     @Test
387     public void testNext() {
388         logger.info("testNext");
389
390         testNext(false, false);
391     }
392
393     @Test
394     public void testNextRepeat() {
395         logger.info("testNextRepeat");
396
397         testNext(false, true);
398     }
399
400     @Test
401     public void testNextWhilePlaying() {
402         logger.info("testNextWhilePlaying");
403
404         testNext(true, false);
405     }
406
407     @Test
408     public void testNextWhilePlayingRepeat() {
409         logger.info("testNextWhilePlayingRepeat");
410
411         testNext(true, true);
412     }
413
414     private void testNext(boolean play, boolean repeat) {
415         // Register a media queue
416         expectLastChangeOnSetAVTransportURI(true, 0);
417         handler.registerQueue(requireNonNull(upnpEntryQueue));
418
419         if (repeat) {
420             handler.handleCommand(repeatChannelUID, OnOffType.ON);
421         }
422
423         if (play) {
424             // Play media
425             handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
426         }
427
428         // Next media
429         expectLastChangeOnSetAVTransportURI(true, 1);
430         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
431
432         checkInternalState(1, 2, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
433         checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
434         checkSetURI(1, 2);
435         checkMetadataChannels(1);
436
437         // Next media
438         expectLastChangeOnSetAVTransportURI(true, 2);
439         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
440
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);
446
447         // Next media
448         expectLastChangeOnSetAVTransportURI(true, 0);
449         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
450
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);
454         checkSetURI(0, 1);
455         checkMetadataChannels(0);
456     }
457
458     @Test
459     public void testPrevious() {
460         logger.info("testPrevious");
461
462         testPrevious(false, false);
463     }
464
465     @Test
466     public void testPreviousRepeat() {
467         logger.info("testPreviousRepeat");
468
469         testPrevious(false, true);
470     }
471
472     @Test
473     public void testPreviousWhilePlaying() {
474         logger.info("testPreviousWhilePlaying");
475
476         testPrevious(true, false);
477     }
478
479     @Test
480     public void testPreviousWhilePlayingRepeat() {
481         logger.info("testPreviousWhilePlayingRepeat");
482
483         testPrevious(true, true);
484     }
485
486     public void testPrevious(boolean play, boolean repeat) {
487         // Register a media queue
488         expectLastChangeOnSetAVTransportURI(true, 0);
489         handler.registerQueue(requireNonNull(upnpEntryQueue));
490
491         if (repeat) {
492             handler.handleCommand(repeatChannelUID, OnOffType.ON);
493         }
494
495         if (play) {
496             // Play media
497             handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
498         }
499
500         // Next media
501         expectLastChangeOnSetAVTransportURI(true, 1);
502         handler.handleCommand(controlChannelUID, NextPreviousType.NEXT);
503
504         // Previous media
505         expectLastChangeOnSetAVTransportURI(true, 2);
506         handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
507
508         checkInternalState(0, 1, play ? false : true, play ? true : false, play ? false : true, play ? true : false);
509         checkControlChannel(play ? PlayPauseType.PLAY : PlayPauseType.PAUSE);
510         checkSetURI(0, 1);
511         checkMetadataChannels(0);
512
513         // Previous media
514         expectLastChangeOnSetAVTransportURI(true, 0);
515         handler.handleCommand(controlChannelUID, NextPreviousType.PREVIOUS);
516
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);
522     }
523
524     @Test
525     public void testAutoPlayNextInQueue() {
526         logger.info("testAutoPlayNextInQueue");
527
528         // Register a media queue
529         expectLastChangeOnSetAVTransportURI(true, 0);
530         handler.registerQueue(requireNonNull(upnpEntryQueue));
531
532         // Play media
533         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
534
535         // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
536         expectLastChangeOnSetAVTransportURI(true, 1);
537
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");
542
543         checkInternalState(1, 2, false, true, false, true);
544         checkControlChannel(PlayPauseType.PLAY);
545         checkSetURI(1, 2);
546         checkMetadataChannels(1);
547     }
548
549     @Test
550     public void testAutoPlayNextInQueueGapless() {
551         logger.info("testAutoPlayNextInQueueGapless");
552
553         // Register a media queue
554         expectLastChangeOnSetAVTransportURI(true, 0);
555         handler.registerQueue(requireNonNull(upnpEntryQueue));
556
557         // Play media
558         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
559
560         // We expect GENA LastChange event with new metadata when the renderer starts to play next entry
561         expectLastChangeOnSetAVTransportURI(true, 1);
562
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");
570
571         checkInternalState(1, 2, false, true, false, true);
572         checkControlChannel(PlayPauseType.PLAY);
573         checkSetURI(null, 2);
574         checkMetadataChannels(1);
575     }
576
577     @Test
578     public void testOnlyPlayOne() {
579         logger.info("testOnlyPlayOne");
580
581         handler.handleCommand(onlyPlayOneChannelUID, OnOffType.ON);
582
583         // Register a media queue
584         expectLastChangeOnSetAVTransportURI(true, 0);
585         handler.registerQueue(requireNonNull(upnpEntryQueue));
586
587         // Play media
588         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
589
590         checkInternalState(0, 1, false, true, false, true);
591         checkSetURI(0, null);
592         checkMetadataChannels(0);
593
594         // We expect GENA LastChange event with new metadata when the renderer has finished playing
595         expectLastChangeOnSetAVTransportURI(true, 1);
596
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");
601
602         checkInternalState(1, 2, false, false, false, true);
603         checkControlChannel(PlayPauseType.PAUSE);
604         checkSetURI(1, null);
605         checkMetadataChannels(1);
606     }
607
608     @Test
609     public void testPlayUri() {
610         logger.info("testPlayUri");
611
612         expectLastChangeOnSetAVTransportURI(true, false, 0);
613         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
614
615         // Play media
616         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
617
618         checkInternalState(null, null, false, true, false, false);
619         checkControlChannel(PlayPauseType.PLAY);
620         checkSetURI(0, null, false);
621         checkMetadataChannels(0, true);
622     }
623
624     @Test
625     public void testPlayAction() {
626         logger.info("testPlayAction");
627
628         expectLastChangeOnSetAVTransportURI(true, false, 0);
629
630         // Methods called in sequence by audio sink
631         handler.setCurrentURI(upnpEntryQueue.get(0).getRes(), "");
632         handler.play();
633
634         checkInternalState(null, null, false, true, false, false);
635         checkControlChannel(PlayPauseType.PLAY);
636         checkSetURI(0, null, false);
637         checkMetadataChannels(0, true);
638     }
639
640     @Test
641     public void testPlayNotification() {
642         logger.info("testPlayNotification");
643
644         // Register a media queue
645         expectLastChangeOnSetAVTransportURI(true, 0);
646         handler.registerQueue(requireNonNull(upnpEntryQueue));
647
648         // Set volume
649         expectLastChangeOnSetVolume(true, 50);
650         handler.setVolume(new PercentType(50));
651
652         checkInternalState(0, 1, true, false, true, false);
653         checkSetURI(0, 1, true);
654         checkMetadataChannels(0, false);
655
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());
660
661         checkInternalState(0, 1, true, false, true, false);
662         checkSetURI(2, null, false);
663         checkMetadataChannels(0, false);
664         verify(handler).setVolume(new PercentType(55));
665
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");
671
672         checkInternalState(0, 1, true, false, true, false);
673         checkMetadataChannels(0, false);
674         verify(handler, times(2)).setVolume(new PercentType(50));
675
676         // Play media and move to position
677         handler.handleCommand(controlChannelUID, PlayPauseType.PLAY);
678
679         checkInternalState(0, 1, false, true, false, true); //
680         checkSetURI(0, 1, true);
681         checkMetadataChannels(0, false);
682
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());
689
690         checkInternalState(0, 1, false, true, false, true);
691         checkSetURI(2, null, false);
692         checkMetadataChannels(0, false);
693         verify(handler).setVolume(new PercentType(70));
694
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);
698         try {
699             TimeUnit.SECONDS.sleep(1);
700             logger.info("Test playing {}, stopped {}", handler.playing, handler.playerStopped);
701         } catch (InterruptedException ignore) {
702         }
703
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));
709     }
710
711     @Test
712     public void testFavorite() {
713         logger.info("testFavorite");
714
715         // Check already called in initialize
716         verify(handler).updateFavoritesList();
717
718         // First set URI
719         expectLastChangeOnSetAVTransportURI(true, false, 0);
720         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(0).getRes()));
721
722         // Save favorite
723         handler.handleCommand(favoriteChannelUID, StringType.valueOf("Test_Favorite"));
724         handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("SAVE"));
725
726         // Check called after saving favorite
727         verify(handler, times(2)).updateFavoritesList();
728
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"));
736
737         // Clear FAVORITE channel
738         handler.handleCommand(favoriteChannelUID, StringType.valueOf(""));
739
740         // Set another URI
741         expectLastChangeOnSetAVTransportURI(true, false, 2);
742         handler.handleCommand(uriChannelUID, StringType.valueOf(upnpEntryQueue.get(2).getRes()));
743
744         checkInternalState(null, null, false, true, false, false);
745         checkSetURI(2, null, false);
746         checkMetadataChannels(2, true);
747
748         // Restore favorite
749         expectLastChangeOnSetAVTransportURI(true, false, 0);
750         handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
751
752         checkInternalState(null, null, false, true, false, false);
753         checkControlChannel(PlayPauseType.PLAY);
754         checkSetURI(0, null, false);
755         checkMetadataChannels(0, true);
756
757         // Delete favorite
758         handler.handleCommand(favoriteSelectChannelUID, StringType.valueOf("Test_Favorite"));
759         handler.handleCommand(favoriteActionChannelUID, StringType.valueOf("DELETE"));
760
761         // Check called after deleting favorite
762         verify(handler, times(3)).updateFavoritesList();
763
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));
769     }
770
771     private void expectLastChangeOnStop(boolean respond) {
772         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "STOPPED" + CLOSE + LAST_CHANGE_FOOTER;
773         doAnswer(invocation -> {
774             if (respond) {
775                 handler.onValueReceived("LastChange", value, "AVTransport");
776             }
777             return Collections.emptyMap();
778         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Stop"), anyMap());
779     }
780
781     private void expectLastChangeOnPlay(boolean respond) {
782         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PLAYING" + CLOSE + LAST_CHANGE_FOOTER;
783         doAnswer(invocation -> {
784             if (respond) {
785                 handler.onValueReceived("LastChange", value, "AVTransport");
786             }
787             return Collections.emptyMap();
788         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Play"), anyMap());
789     }
790
791     private void expectLastChangeOnPause(boolean respond) {
792         String value = LAST_CHANGE_HEADER + TRANSPORT_STATE + "PAUSED_PLAYBACK" + CLOSE + LAST_CHANGE_FOOTER;
793         doAnswer(invocation -> {
794             if (respond) {
795                 handler.onValueReceived("LastChange", value, "AVTransport");
796             }
797             return Collections.emptyMap();
798         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("Pause"), anyMap());
799     }
800
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 -> {
807             if (respond) {
808                 handler.onValueReceived(UPNP_MASTER + "Volume", String.valueOf(volume), "RenderingControl");
809             }
810             return Collections.emptyMap();
811         }).when(upnpIOService).invokeAction(eq(handler), eq("RenderingControl"), eq("SetVolume"), eq(inputs));
812     }
813
814     private void expectLastChangeOnGetPositionInfo(boolean respond, String seekTarget) {
815         Map<String, String> inputs = new HashMap<>();
816         inputs.put("InstanceID", "0");
817         doAnswer(invocation -> {
818             if (respond) {
819                 handler.onValueReceived("RelTime", seekTarget, "AVTransport");
820             }
821             return Collections.emptyMap();
822         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("GetPositionInfo"), eq(inputs));
823     }
824
825     private void expectLastChangeOnSetAVTransportURI(boolean respond, int mediaId) {
826         expectLastChangeOnSetAVTransportURI(respond, true, mediaId);
827     }
828
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 -> {
840             if (respond) {
841                 handler.onValueReceived("LastChange", value, "AVTransport");
842             }
843             return Collections.emptyMap();
844         }).when(upnpIOService).invokeAction(eq(handler), eq("AVTransport"), eq("SetAVTransportURI"), eq(inputs));
845     }
846
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);
851         } else {
852             assertThat(handler.currentEntry, is(upnpEntryQueue.get(currentEntry)));
853         }
854         if (nextEntry == null) {
855             assertNull(handler.nextEntry);
856         } else {
857             assertThat(handler.nextEntry, is(upnpEntryQueue.get(nextEntry)));
858         }
859         assertThat(handler.playerStopped, is(playerStopped));
860         assertThat(handler.playing, is(playing));
861         assertThat(handler.registeredQueue, is(registeredQueue));
862         assertThat(handler.playingQueue, is(playingQueue));
863     }
864
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));
869     }
870
871     private void checkSetURI(@Nullable Integer current, @Nullable Integer next) {
872         checkSetURI(current, next, true);
873     }
874
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()));
881             if (withMetadata) {
882                 assertThat(metadataCaptor.getValue(),
883                         is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(current)))));
884             }
885         }
886         if (next != null) {
887             verify(handler, atLeastOnce()).setNextURI(uriCaptor.capture(), metadataCaptor.capture());
888             assertThat(uriCaptor.getValue(), is(upnpEntryQueue.get(next).getRes()));
889             if (withMetadata) {
890                 assertThat(metadataCaptor.getValue(),
891                         is(UpnpXMLParser.compileMetadataString(requireNonNull(upnpEntryQueue.get(next)))));
892             }
893         }
894     }
895
896     private void checkMetadataChannels(int mediaId) {
897         checkMetadataChannels(mediaId, false);
898     }
899
900     private void checkMetadataChannels(int mediaId, boolean cleared) {
901         ArgumentCaptor<State> stateCaptor = ArgumentCaptor.forClass(State.class);
902
903         verify(callback, atLeastOnce()).stateUpdated(eq(thing.getChannel(URI).getUID()), stateCaptor.capture());
904         assertThat(stateCaptor.getValue(), is(StringType.valueOf(upnpEntryQueue.get(mediaId).getRes())));
905
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));
930         }
931     }
932 }