@@ -130,15 +130,17 @@ async def get_config_entries(
130130 """Return all (provider/player specific) Config Entries for the given player (if any)."""
131131 base_entries = await super ().get_config_entries ()
132132
133+ require_pairing = self ._requires_pairing ()
134+
133135 # Handle pairing actions
134- if action and self . _requires_pairing () :
136+ if action and require_pairing :
135137 await self ._handle_pairing_action (action = action , values = values )
136138
137139 # Add pairing config entries for Apple TV and macOS devices
138- if self . _requires_pairing () :
139- base_entries = [* self ._get_pairing_config_entries (), * base_entries ]
140+ if require_pairing :
141+ base_entries = [* self ._get_pairing_config_entries (values ), * base_entries ]
140142
141- base_entries = await super (). get_config_entries ( action = action , values = values )
143+ # Regular AirPlay config entries
142144 base_entries += [
143145 CONF_ENTRY_FLOW_MODE_ENFORCED ,
144146 CONF_ENTRY_DEPRECATED_EQ_BASS ,
@@ -223,71 +225,6 @@ async def get_config_entries(
223225 ),
224226 ]
225227
226- # Regular AirPlay config entries
227- base_entries .extend (
228- [
229- ConfigEntry (
230- key = CONF_ENCRYPTION ,
231- type = ConfigEntryType .BOOLEAN ,
232- default_value = True ,
233- label = "Enable encryption" ,
234- description = "Enable encrypted communication with the player, "
235- "should by default be enabled for most devices." ,
236- category = "airplay" ,
237- ),
238- ConfigEntry (
239- key = CONF_ALAC_ENCODE ,
240- type = ConfigEntryType .BOOLEAN ,
241- default_value = True ,
242- label = "Enable compression" ,
243- description = "Save some network bandwidth by sending the audio as "
244- "(lossless) ALAC at the cost of a bit CPU." ,
245- category = "airplay" ,
246- ),
247- CONF_ENTRY_SYNC_ADJUST ,
248- ConfigEntry (
249- key = CONF_PASSWORD ,
250- type = ConfigEntryType .SECURE_STRING ,
251- default_value = None ,
252- required = False ,
253- label = "Device password" ,
254- description = "Some devices require a password to connect/play." ,
255- category = "airplay" ,
256- ),
257- ConfigEntry (
258- key = CONF_READ_AHEAD_BUFFER ,
259- type = ConfigEntryType .INTEGER ,
260- default_value = 1000 ,
261- required = False ,
262- label = "Audio buffer (ms)" ,
263- description = "Amount of buffer (in milliseconds), "
264- "the player should keep to absorb network throughput jitter. "
265- "If you experience audio dropouts, try increasing this value." ,
266- category = "airplay" ,
267- range = (500 , 3000 ),
268- ),
269- # airplay has fixed sample rate/bit depth so make this config entry
270- # static and hidden
271- create_sample_rates_config_entry (
272- supported_sample_rates = [44100 ], supported_bit_depths = [16 ], hidden = True
273- ),
274- ConfigEntry (
275- key = CONF_IGNORE_VOLUME ,
276- type = ConfigEntryType .BOOLEAN ,
277- default_value = False ,
278- label = "Ignore volume reports sent by the device itself" ,
279- description = (
280- "The AirPlay protocol allows devices to report their own volume "
281- "level. \n "
282- "For some devices this is not reliable and can cause unexpected "
283- "volume changes. \n "
284- "Enable this option to ignore these reports."
285- ),
286- category = "airplay" ,
287- ),
288- ]
289- )
290-
291228 if is_broken_raop_model (self .device_info .manufacturer , self .device_info .model ):
292229 base_entries .insert (- 1 , BROKEN_RAOP_WARN )
293230
@@ -305,67 +242,91 @@ def _requires_pairing(self) -> bool:
305242 # Mac devices (including iMac, MacBook, Mac mini, Mac Pro, Mac Studio)
306243 return model .startswith (("Mac" , "iMac" ))
307244
308- def _get_pairing_config_entries (self ) -> list [ConfigEntry ]:
245+ def _get_pairing_config_entries (
246+ self , values : dict [str , ConfigValueType ] | None
247+ ) -> list [ConfigEntry ]:
309248 """Return pairing config entries for Apple TV and macOS devices.
310249
311250 Uses cliraop for AirPlay/RAOP pairing.
312251 """
313252 entries : list [ConfigEntry ] = []
314253
315254 # Check if we have credentials stored
316- has_credentials = bool (self .config .get_value (CONF_AP_CREDENTIALS ))
255+ if values and (creds := values .get (CONF_AP_CREDENTIALS )):
256+ credentials = str (creds )
257+ else :
258+ credentials = str (self .config .get_value (CONF_AP_CREDENTIALS ) or "" )
259+ has_credentials = bool (credentials )
317260
318261 if not has_credentials :
319262 # Show pairing instructions and start button
320- entries .append (
321- ConfigEntry (
322- key = "pairing_instructions" ,
323- type = ConfigEntryType .LABEL ,
324- label = "AirPlay Pairing Required" ,
325- description = (
326- "This device requires pairing before it can be used. "
327- "Click the button below to start the pairing process."
328- ),
263+ if not self .stream and self .protocol == StreamingProtocol .RAOP :
264+ # ensure we have a stream instance to track pairing state
265+ from .protocols .raop import RaopStream # noqa: PLC0415
266+
267+ self .stream = RaopStream (self )
268+ elif not self .stream and self .protocol == StreamingProtocol .AIRPLAY2 :
269+ # ensure we have a stream instance to track pairing state
270+ from .protocols .airplay2 import AirPlay2Stream # noqa: PLC0415
271+
272+ self .stream = AirPlay2Stream (self )
273+ if self .stream and not self .stream .supports_pairing :
274+ # TEMP until ap2 pairing is implemented
275+ return [
276+ ConfigEntry (
277+ key = "pairing_unsupported" ,
278+ type = ConfigEntryType .ALERT ,
279+ label = (
280+ "This device requires pairing but it is not supported "
281+ "by the current Music Assistant AirPlay implementation."
282+ ),
283+ )
284+ ]
285+
286+ # If pairing was started, show PIN entry
287+ if self .stream and self .stream .is_pairing :
288+ entries .append (
289+ ConfigEntry (
290+ key = CONF_PAIRING_PIN ,
291+ type = ConfigEntryType .STRING ,
292+ label = "Enter the 4-digit PIN shown on the device" ,
293+ required = True ,
294+ )
329295 )
330- )
331- entries .append (
332- ConfigEntry (
333- key = CONF_ACTION_START_PAIRING ,
334- type = ConfigEntryType .ACTION ,
335- label = "Start Pairing" ,
336- description = "Start the AirPlay pairing process" ,
337- action = CONF_ACTION_START_PAIRING ,
296+ entries .append (
297+ ConfigEntry (
298+ key = CONF_ACTION_FINISH_PAIRING ,
299+ type = ConfigEntryType .ACTION ,
300+ label = "Complete the pairing process with the PIN" ,
301+ action = CONF_ACTION_FINISH_PAIRING ,
302+ )
303+ )
304+ else :
305+ entries .append (
306+ ConfigEntry (
307+ key = "pairing_instructions" ,
308+ type = ConfigEntryType .LABEL ,
309+ label = (
310+ "This device requires pairing before it can be used. "
311+ "Click the button below to start the pairing process."
312+ ),
313+ )
314+ )
315+ entries .append (
316+ ConfigEntry (
317+ key = CONF_ACTION_START_PAIRING ,
318+ type = ConfigEntryType .ACTION ,
319+ label = "Start the AirPlay pairing process" ,
320+ action = CONF_ACTION_START_PAIRING ,
321+ )
338322 )
339- )
340323 else :
341324 # Show paired status
342325 entries .append (
343326 ConfigEntry (
344327 key = "pairing_status" ,
345328 type = ConfigEntryType .LABEL ,
346- label = "AirPlay Pairing Status" ,
347- description = "Device is paired and ready to use." ,
348- )
349- )
350-
351- # If pairing was started, show PIN entry
352- if self .config .get_value ("_pairing_in_progress" ):
353- entries .append (
354- ConfigEntry (
355- key = CONF_PAIRING_PIN ,
356- type = ConfigEntryType .STRING ,
357- label = "Enter PIN" ,
358- description = "Enter the 4-digit PIN shown on the device" ,
359- required = True ,
360- )
361- )
362- entries .append (
363- ConfigEntry (
364- key = CONF_ACTION_FINISH_PAIRING ,
365- type = ConfigEntryType .ACTION ,
366- label = "Finish Pairing" ,
367- description = "Complete the pairing process with the PIN" ,
368- action = CONF_ACTION_FINISH_PAIRING ,
329+ label = "Device is paired and ready to use." ,
369330 )
370331 )
371332
@@ -375,7 +336,8 @@ def _get_pairing_config_entries(self) -> list[ConfigEntry]:
375336 key = CONF_AP_CREDENTIALS ,
376337 type = ConfigEntryType .SECURE_STRING ,
377338 label = "AirPlay Credentials" ,
378- default_value = None ,
339+ default_value = credentials ,
340+ value = credentials ,
379341 required = False ,
380342 hidden = True ,
381343 )
@@ -386,41 +348,44 @@ def _get_pairing_config_entries(self) -> list[ConfigEntry]:
386348 async def _handle_pairing_action (
387349 self , action : str , values : dict [str , ConfigValueType ] | None
388350 ) -> None :
389- """Handle pairing actions using cliraop.
351+ """Handle pairing actions using the configured protocol."""
352+ if not self .stream and self .protocol == StreamingProtocol .RAOP :
353+ # ensure we have a stream instance to track pairing state
354+ from .protocols .raop import RaopStream # noqa: PLC0415
390355
391- TODO: Implement actual cliraop-based pairing.
392- """
356+ self .stream = RaopStream (self )
357+ elif not self .stream and self .protocol == StreamingProtocol .AIRPLAY2 :
358+ # ensure we have a stream instance to track pairing state
359+ from .protocols .airplay2 import AirPlay2Stream # noqa: PLC0415
360+
361+ self .stream = AirPlay2Stream (self )
393362 if action == CONF_ACTION_START_PAIRING :
394- # TODO: Start pairing using cliraop
395- # For now, just set a flag to show the PIN entry
396- self .mass .config .set_raw_player_config_value (
397- self .player_id , "_pairing_in_progress" , True
398- )
363+ if self .stream and self .stream .is_pairing :
364+ self .logger .warning ("Pairing process already in progress for %s" , self .display_name )
365+ return
399366 self .logger .info ("Started AirPlay pairing for %s" , self .display_name )
367+ if self .stream :
368+ await self .stream .start_pairing ()
400369
401370 elif action == CONF_ACTION_FINISH_PAIRING :
402- # TODO: Finish pairing using cliraop with the provided PIN
403371 if not values :
372+ # guard
404373 return
405374
406375 pin = values .get (CONF_PAIRING_PIN )
407376 if not pin :
408377 self .logger .warning ("No PIN provided for pairing" )
409378 return
410379
411- # TODO: Use cliraop to complete pairing with the PIN
412- # For now, just clear the pairing in progress flag
413- self .mass .config .set_raw_player_config_value (
414- self .player_id , "_pairing_in_progress" , False
415- )
380+ if self .stream :
381+ credentials = await self .stream .finish_pairing (pin = str (pin ))
382+ else :
383+ return
416384
417- # TODO: Store the actual credentials obtained from cliraop
418- # self.mass.config.set_raw_player_config_value(
419- # self.player_id, CONF_AP_CREDENTIALS, credentials_from_cliraop
420- # )
385+ values [CONF_AP_CREDENTIALS ] = credentials
421386
422387 self .logger .info (
423- "Finished AirPlay pairing for %s (TODO: implement actual pairing) " ,
388+ "Finished AirPlay pairing for %s" ,
424389 self .display_name ,
425390 )
426391
@@ -522,7 +487,7 @@ async def play_media(self, media: PlayerMedia) -> None:
522487 if self .stream .prevent_playback :
523488 # player is in prevent playback mode, we need to stop the stream
524489 await self .stop ()
525- else :
490+ elif self . stream . session :
526491 await self .stream .session .replace_stream (audio_source )
527492 return
528493
@@ -564,7 +529,7 @@ async def set_members(
564529 if player_ids_to_remove :
565530 if self .player_id in player_ids_to_remove :
566531 # dissolve the entire sync group
567- if self .stream and self .stream .running :
532+ if self .stream and self .stream .running and self . stream . session :
568533 # stop the stream session if it is running
569534 await self .stream .session .stop ()
570535 self ._attr_group_members = []
@@ -598,6 +563,7 @@ async def set_members(
598563 if (
599564 child_player_to_add .stream
600565 and child_player_to_add .stream .running
566+ and child_player_to_add .stream .session
601567 and child_player_to_add .stream .session != stream_session
602568 ):
603569 await child_player_to_add .stream .session .remove_client (child_player_to_add )
@@ -663,7 +629,7 @@ async def on_unload(self) -> None:
663629 await super ().on_unload ()
664630 if self .stream :
665631 # stop the stream session if it is running
666- if self .stream .running :
632+ if self .stream .running and self . stream . session :
667633 self .mass .create_task (self .stream .session .stop ())
668634 self .stream = None
669635
0 commit comments