+
    G.jy                        R t ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	t	^ RI
t
^ RIt^ RIt^ RIt^ RIHt ^ RIt]P$                  ! ]P&                  P(                  4       ]P*                  ! R4      tRsRtRtRtRtR	tR
tRRRRRRRRRRRRRRRRRRRRRRRR R!R"R#R$/tR%t ] ! R&4      ;_uu_ 4       t!]! FC  t"]"PG                  R'4      '       g   K  ]"PI                  4       PK                  R(^4      ^,          tKE  	  RRR4       R)t'R* R+ lt(R, R- lt)RR. R/ llt*RR0 R1 llt+RR2 R3 llt,RR4 R5 llt-RR6 R7 llt.RR8 R9 llt/RR: R; llt0^<t1RR< R= llt2RR> R? llt3RR@ RA llt4RRB RC llt5RRD RE llt6RRF RG llt7RRH RI llt8RRJ RK llt9RRL RM llt:RRN RO llt;RRP RQ llt<RRR RS llt=RRT RU llt>RRV RW llt?RX RY lt@RZ R[ ltAR\ R] ltBRR^ R_ lltCRR` Ra lltDRRb Rc lltE. ROtFRd Re ltGRftHRgtIRhtJRitKRjtL^ RIMtN^ RIOHPu HQtR Rk Rl ltSRm Rn ltTRRo Rp lltURq Rr ltVRs Rt ltW\.        Ru3Rv Rw lltXRu\.        3Rx Ry lltYRRz R{ lltZRR| R} llt[R~ R lt\ ! R R4      t] ! R R]&4      t^ ! R R]&4      t_R R lt` ! R R4      taRR R lltbRRu\.        3R R lltcR#   + '       g   i     EL; i  ]& d     ELi ; i)u3  Shared TV control module — JointSpace, UPnP, ADB, Home Assistant, throttle.

Consolidates all TV/ADB/HA control functions used by sandman.py, sandman_bot.py,
and devices_server.py.  Import and call directly:

    import tvmod as tv_control
    tv_control.js_key("Pause")
    vol = tv_control.get_volume()
NHTTPDigestAuth
tv_controlz192.168.1.50a1b2c3d4e5f6@8f075a1826341135dcd3a9dd2ed30a49c327d7e002399fbaefae28d8e1f9936fi  z/urn:schemas-upnp-org:service:RenderingControl:1z<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>{body}</s:Body></s:Envelope>  com.google.android.youtube.tvYouTubecom.netflix.ninjaNetflix!com.amazon.amazonvideo.livingroomzPrime Videozorg.droidtv.channelszLive TVorg.droidtv.playtvzorg.droidtv.contentexplorerzMedia Browser com.apple.atve.androidtv.appletvz	Apple TV+z$com.google.android.apps.tv.launcherxHomezcom.google.android.tvlauncherzcom.google.android.katnissz	Google TVzorg.droidtv.settingsSettingscom.disney.disneypluszDisney+z
tv.mewatchmewatchzcom.spotify.tv.androidSpotify z/data/.ssh/environmentzSUPERVISOR_TOKEN==z/share/.ha_cache/.ha_wd_tv_ipc                $    V ^8  d   QhR\         /#    returnstr)formats   "/share/.ha_cache/tvmod.py__annotate__r   R   s     
 
C 
    c                      \        \        4      P                  4       P                  4       p V '       d   V s\        #   \
         d	     \        # i ; i)zOReturn the current TV IP, checking cache file if module-level TV_IP is default.)openTV_IP_CACHEreadstripTV_IP	Exception)cacheds    r   
_get_tv_ipr(   R   sK    k"'')//1E L  Ls   5= AAc                $    V ^8  d   QhR\         /# r   r   )r   s   "r   r   r   a   s     5 5. 5r   c                  *    \        \        \        4      # N)r   JS_DEVICE_IDJS_AUTH_KEY r   r   _js_authr/   a   s    ,44r   c                0    V ^8  d   QhR\         R\         /# r   ipr   r   )r   s   "r   r   r   e   s     " " " "r   c                 6    T ;'       g    \        4       p R V  R2# )https://z:1926/6)r(   r2   s   &r   _js_baser6   e   s     			z|BbT!!r   c                0    V ^8  d   QhR\         R\         /# r1   r   )r   s   "r   r   r   j   s     % %C %3 %r   c                 @    T ;'       g    \        4       p V  R \         2# ):)r(   ADB_TARGET_PORTr5   s   &r   _adb_targetr;   j   s#    			z|BT?#$$r   c                0    V ^8  d   QhR\         R\         /# r1   r   )r   s   "r   r   r   o   s     E E# E Er   c                 D    T ;'       g    \        4       p R V  R\         R2# )http://r9   z/upnp/control/RenderingControl1)r(   	UPNP_PORTr5   s   &r   	_upnp_urlr@   o   s'    			z|BRD)$CDDr   c          	      V    V ^8  d   QhR\         R\        R\         R\        R,          /# )r   pathtimeoutr2   r   N)r   floatdict)r   s   "r   r   r   v   s,     
 
 
u 
c 
TD[ 
r   c                L   T;'       g    \        4       p \        V4       RV P                  R4       2p\        P                  ! V\        4       RVR7      pVP                  4        VP                  4       #   \         d"   p\        P                  RY4        Rp?R# Rp?ii ; i)z8GET a JointSpace endpoint.  Returns parsed JSON or None./F)authverifyrC   zjs_get /%s failed: %sN)r(   r6   lstriprequestsgetr/   raise_for_statusjsonr&   logdebug)rB   rC   r2   urlres   &&&   r   js_getrT   v   s    			z|B"aC 012LL8:eWM	vvx 		)43s   A A7 7B#BB#c          
      T    V ^8  d   QhR\         R\        R\        R\         R\        /# )r   rB   datarC   r2   r   r   rE   rD   bool)r   s   "r   r   r      s/     	 	# 	T 	E 	3 	$ 	r   c                *   T;'       g    \        4       p \        V4       RV P                  R4       2p\        P                  ! WA\        4       RVR7      pVP                  R8  #   \         d"   p\        P                  RY4        Rp?R# Rp?ii ; i)z8POST to a JointSpace endpoint.  Returns True on success.rG   F)rN   rH   rI   rC     zjs_post /%s failed: %sN)
r(   r6   rJ   rK   postr/   status_coder&   rO   rP   )rB   rV   rC   r2   rQ   rR   rS   s   &&&&   r   js_postr]      s|    			z|B"aC 012MM#xz%QXY}}s"" 		*D4s   AA& &B1BBc                <    V ^8  d   QhR\         R\         R\        /# )r   keyr2   r   r   rX   )r   s   "r   r   r      s!     5 5 5 5 5r   c                "    \        RRV /VR7      # )z Send a key press via JointSpace.z	input/keyr_   r5   )r]   )r_   r2   s   &&r   js_keyrb      s    ;44r   c          	      V    V ^8  d   QhR\         R\         R\         R\         R,          /# )r   action
body_innerr2   r   Nr   )r   s   "r   r   r      s,      # 3 C 3: r   c                   T;'       g    \        4       p\        V4      p\        P                  VR7      pRRRR\         RV  R2/p \
        P                  P                  W4P                  4       VRR7      p\
        P                  P                  V^R	7      ;_uu_ 4       pVP                  4       P                  4       uuR
R
R
4       #   + '       g   i     R
# ; i  \         d"   p\        P                  RY4        R
p?R
# R
p?ii ; i)zMSend a UPnP SOAP request to RenderingControl.  Returns response body or None.)bodyContent-Typeztext/xml; charset="utf-8"
SOAPAction"#POSTrV   headersmethodrC   NzUPnP %s failed: %s)r(   r@   UPNP_ENVELOPEr   UPNP_SVCurllibrequestRequestencodeurlopenr#   decoder&   rO   rP   )	rd   re   r2   rQ   rg   rn   reqresprS   s	   &&&      r   _upnp_requestr{      s    			z|B
B-CZ0D3(1VHA.Gnn$$S{{}gV\$]^^##C#33t99;%%' 4333 		&2s7   AC !C
?
C 
C	C C D
)DD
c                >    V ^8  d   QhR\         R\        R,          /# r   r2   r   N)r   int)r   s   "r   r   r      s     
5 
53 
5#* 
5r   c                    R\          R2p\        RWR7      pVf   R# \        P                  ! RV4      pV'       g   R# \	        \        VP                  ^4      4      \        ,          ^d,          4      # )zGet current volume on the TV's native 0-VOLUME_MAX scale (matches the OSD).
Reads UPnP RenderingControl (0-100) and rescales.  Returns int or None.z<u:GetVolume xmlns:u="zC"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetVolume>	GetVolumer5   Nz$<CurrentVolume>(\d+)</CurrentVolume>)rr   r{   researchroundr~   group
VOLUME_MAXr2   rg   rz   ms   &   r   
get_volumer      s_     $H:-pqDd2D|
		94@AQWWQZ:-344r   c                <    V ^8  d   QhR\         R\        R\        /# )r   volr2   r   )r~   r   rX   )r   s   "r   r   r      s!     ? ?C ?S ?D ?r   c                    \        ^ \        \        V 4      4      p \        V ^d,          \        ,          4      pR\         RV R2p\        RW1R7      RJ# )zfSet volume on the TV's native 0-VOLUME_MAX scale (matches the OSD).
Rescales to UPnP 0-100 internally.z<u:SetVolume xmlns:u="zD"><InstanceID>0</InstanceID><Channel>Master</Channel><DesiredVolume>z</DesiredVolume></u:SetVolume>	SetVolumer5   N)maxminr   r   rr   r{   )r   r2   upnprg   s   &&  r   
set_volumer      sZ     aZ%
&CsZ'(D$XJ /"V#ACD d2$>>r   c                >    V ^8  d   QhR\         R\        R,          /# r}   r`   )r   s   "r   r   r      s     . . .t .r   c                    R\          R2p\        RWR7      pVf   R# \        P                  ! RV4      pV'       d   VP	                  ^4      R8H  # R# )z/Get mute state via UPnP.  Returns bool or None.z<u:GetMute xmlns:u="zA"><InstanceID>0</InstanceID><Channel>Master</Channel></u:GetMute>GetMuter5   Nz <CurrentMute>(\d+)</CurrentMute>1)rr   r{   r   r   r   r   s   &   r   get_muter      sP    !(+lmDD0D|
		5t<A"#AGGAJ#--r   c                <    V ^8  d   QhR\         R\        R\         /# )r   mutedr2   r   )rX   r   )r   s   "r   r   r      s!     = =D =c =T =r   c                R    V '       d   RMRpR\          RV R2p\        RW1R7      RJ# )	zSet mute state via UPnP.r   0z<u:SetMute xmlns:u="zB"><InstanceID>0</InstanceID><Channel>Master</Channel><DesiredMute>z</DesiredMute></u:SetMute>SetMuter5   N)rr   r{   )r   r2   valrg   s   &&  r   set_muter      s:    #CC"8* -5 :<D D0<<r   c                0    V ^8  d   QhR\         R\         /# r1   r   )r   s   "r   r   r      s       s r   c                p    \        RV R7      pV'       d!   VP                  RR4      P                  4       # R# )z%Return 'On', 'Standby', or 'Unknown'.
powerstater5   Unknown)rT   rL   
capitalize)r2   rV   s   & r   get_power_stater      s.    ,2&Dxxi0;;==r   c                0    V ^8  d   QhR\         R\        /# r1   r   rE   )r   s   "r   r   r      s     K K Kt Kr   c                \    \        V R7      p\        V R7      pRVe   TM^ R\        V4      /# )uw   Return {'volume': int (0-VOLUME_MAX), 'muted': bool}.
Uses UPnP — JointSpace audio/volume is unavailable on this set.r5   volumer   )r   r   rX   )r2   r   r   s   &  r   get_audio_stater      s0     
COES_c!Wd5kJJr   c                >    V ^8  d   QhR\         R\        R,          /# )r   rC   r   NrD   r   )r   s   "r   r   r      s     5 5 5t 5r   c           	         R\        V 4       R2p\        P                  ! \        P                  \        P                  \        P                  4      pVP                  \        P                  \        P                  ^4       VP                  \        V ^4      4       VP                  VP                  4       R4         VP                  R4      w  r4VP                  RR7      pRV9   g   RV9   g   RVP                  4       9   g   KK  VP                  4        \         P#                  R	V^ ,          4       V^ ,           VP                  4        #   \$         d     # i ; i  \        P&                   d     Mi ; i TP                  4        MI  \$         d     M<i ; i   TP                  4        i   \$         d     i i ; i; i  \$         d     Mi ; iR
 p\)        ^^4       Uu. uF  pRT 2NK
  	  Mu upi pp\*        P,                  P/                  ^R7      ;_uu_ 4       p	T U
u/ uF  qP1                  Yj4      T
bK  	  Mu up
i pp
\*        P,                  P3                  T4       F?  pTP5                  4       pT'       g   K  \         P#                  RT4       Tu uuRRR4       # 	  RRR4       R#   + '       g   i     R# ; i)zIFind the Philips TV via SSDP multicast, falling back to JointSpace probe.zKM-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: z
ST: ssdp:all

   replaceerrorsPhilipsPhilipsIntelSDKandroidzSSDP discovered TV at %sc                      \         P                  ! 4       p\        P                  P	                  \        P                  P                  R V  R24      ^VR7       V #   \         d     R# i ; i)r4   z:1926/6/system)rC   contextN)ssl_create_unverified_contextrs   rt   rw   ru   r&   )check_ipctxs   & r   probediscover_tv.<locals>.probe  se    	002CNN""&&(>'JK3 #  O 		s   AA A+*A+z
192.168.1.)max_workerszProbe discovered TV at %sNz239.255.255.250il  )r~   socketAF_INET
SOCK_DGRAMIPPROTO_UDP
setsockopt
IPPROTO_IPIP_MULTICAST_TTL
settimeoutr   sendtorv   recvfromrx   lowercloserO   infor&   rC   range
concurrentfuturesThreadPoolExecutorsubmitas_completedresult)rC   ssdp_msgsockrV   addrtextr   i
candidatespoolr2   futsfr   s   &             r   discover_tvr      sI    w<. !## 	 }}V^^V->->@R@RS))6+B+BAFGQ(HOO%'@A	!]]40
{{){4$(9T(AYRVR\R\R^E^JJLHH7aA7

  ~~ 		

 

  	 -2!RL9LqJqc"L9J9				.	.2	.	>	>$5?@ZrE&*Z@@##006AXXZFv4f= 
?	>6 
?  
?	> s   B;G >AE! 	5E! ?EEG EG !E96F 8E99F =F FG FG G!F21G2G =G?G  GG GG(G7&J9+I?J9	J9-J99K
	c                $    V ^8  d   QhR\         /# )r   r2   r   )r   s   "r   r   r   (  s     6 6 6r   c                ,   T ;'       g    \        4       p  \        P                  ! 4       p\        4       Vn        RVn        \        V 4       R2pVP                  VRR/^R7       \        P                  ! R4       \        ^4       F  pVP                  VRR/^R7       K  	  \        P                  ! R4       VP                  VRR	/^R7       VP                  VRR
/^R7       R#   \         d"   p\        P                  RT4        Rp?R# Rp?ii ; i)zSToggle ambilight via menu navigation (AmbilightOnOff -> up 15x -> Confirm -> Back).Fz
/input/keyr_   AmbilightOnOff)rN   rC   g333333?CursorUpg?ConfirmBackzAmbilight toggle failed: %sN)r(   rK   Sessionr/   rH   rI   r6   r[   timesleepr   r&   rO   warning)r2   srQ   _rS   s   &    r   ambilight_toggler   (  s    			z|B6"j)	s%!12A>

4rAFF3eZ0!F< 

4	s%+Q7	s%!4 611556s   CC' 'D2DDc                0    V ^8  d   QhR\         R\        /# )r   off_durationr2   r   )r   s   "r   r   r   ;  s      5 C r   c                b    \        VR7       \        P                  ! V 4       \        VR7       R# )z+Toggle ambilight off, wait, toggle back on.r5   N)r   r   r   )r   r2   s   &&r   ambilight_glitchr   ;  s    JJ|r   c                0    V ^8  d   QhR\         R\        /# r1   r`   )r   s   "r   r   r   D  s     	 	C 	4 	r   c                6   \        V 4      p \        P                  ! RRV.RR^
R7      pRVP                  P	                  4       9   ;'       g    RVP                  P	                  4       9   #   \
         d"   p\        P                  RT4        Rp?R	# Rp?ii ; i)
z0Connect to TV via ADB.  Returns True on success.adbconnectTcapture_outputr   rC   	connectedalreadyzadb_connect failed: %sNF)r;   
subprocessrunstdoutr   r&   rO   rP   )r2   targetrR   rS   s   &   r   adb_connectr   D  s    _FNNE9f5*.T2Gahhnn..OO)qxx~~?O2OO 		*A.s   A A, A, ,B7BBc                J    V ^8  d   QhR\         R\         R\         R,          /# )r   output_pathr2   r   Nr   )r   s   "r   r   r   P  s&        sUYz r   c           	        \        V4      p \        V4       \        P                  ! RRVRRRR.R^RR7       \        P                  ! RRVR	RV .R^RR7       V #   \         d"   p\
        P                  R
T4        Rp?R# Rp?ii ; i)zKTake a screenshot via ADB.  Returns local path on success, None on failure.r   -sshell	screencapz-pz/sdcard/screen.pngT)r   rC   checkpullzadb_screenshot failed: %sN)r;   r   r   r   r&   rO   r   )r   r2   r   rS   s   &&  r   adb_screenshotr   P  s    _FBD&';>RS4	
 	D&&*>L4	
  /3s   AA B&BBc                0    V ^8  d   QhR\         R\        /# r1   )r   tuple)r   s   "r   r   r   c  s     G  G C G 5 G r   c           	     &   \        V 4      p / p\        P                  ! RRVRRRR.RR^R7      pR	pVP                  P	                  4        FQ  pR
V9   g   K  \
        P                  ! RV4      pV'       d'   VP                  ^4      P                  4       R!,          p M	  R	p\        P                  ! RRVRRR.RR^R7      pRp	VP                  P	                  4        EFQ  pRV9   d   Rp	V	'       Eda   RV9   EdY   RV9   d   RpM	RV9   d   RpV'       Ed=   \
        P                  ! RV4      p
\
        P                  ! RV4      p\
        P                  ! RV4      pV
'       d   V'       d   \        V
P                  ^4      4      p\        VP                  ^4      4      pV'       d   \        VP                  ^4      4      MRp \        P                  ! RRVRR.RR^R7      p\        \        VP                  P                  4       ^ ,          4      R,          4      pVV,
          V,          pVV,           pV^ 8  d   \        VR,          4      VR&   V	'       g   EK  RV9   g   EK  RV9   g   EK  \
        P                  ! RV4      pV'       d   VP                  ^4      P                  4       P                  R4      P                  R4      pVP                  R4       Uu. uF  pVP                  4       NK  	  ppV'       d   V^ ,          VR&   \        V4      ^8  d   V^,          VR&   Rp	EKT  	  WGV3#   \         d    T^ 8  d   TR,          TR&    ELi ; iu upi   \         d#   p\        P!                  R T4       R"u R	p?# R	p?ii ; i)#zGet current foreground app, playback state, and extras via ADB.

Returns (package_name, playback_state, extras_dict) where extras may
contain 'title', 'artist', 'position_s'.
Any element may be None on failure.
r   r   r   dumpsysactivity
activitiesTr   NtopResumedActivityz	(\S+)/\S+media_sessionFzactive=truezstate=PlaybackStatePLAYINGPAUSEDzposition=(\d+)zupdated=(\d+)zspeed=([\d.]+)      ?zcat /proc/uptimei  
position_sz	metadata:zsize=zdescription=(.+?)(?:,\s*null)?$z, null,titleartistzadb_get_current_app failed: %s)NNN)r;   r   r   r   
splitlinesr   r   r   splitr~   rD   r&   r$   rstriplenrO   r   )r2   r   extrasrR   pkgliner   stater2found_activepos_mupd_mspd_mpos_msupdatedspeedup_r	uptime_ms
elapsed_msreal_pos_msdescppartsrS   s   &                       r   adb_get_current_appr(  c  s"    _F?  NNE4%z<A*.T1F HH'')D#t+IIlD1''!***,R0C * ^^UD&'&9+/dAG II((*D$#| 5 =$%E%$E5II&7>EII&6=EII&7>E!$U[[^!4"%ekk!n"59>ekk!n 5CF#->>!&fg?Q R/3$$KD ),E$++2C2C2Ea2H,ID,P(QI*3g*=)FJ*0:*=K*Q7:;$;N7O| 4 |t 34II@$G771:++-44X>EEcJD04

3@1QWWYE@*/(w5zA~+08x($K +N 6!!  ) F%z7=~| 4F A   4a8 s   AM# M# 7A=M# 5#M# AM# &M# .AM# BL9M#  M# *M# 4M# AM# #M<<M# 9MM# MM# #N.NNNc                <    V ^8  d   QhR\         R\         R\        /# )r   packager2   r   r`   )r   s   "r   r   r     s!      C S D r   c                   \        V4      pRRRRRRRRR	R
RR/p \        V4       VP                  V 4      pV'       d%   \        P                  ! RRVRRV R2.R^
R7       R# \        P                  ! RRVRRV  R2.R^
R7      pVP
                  ^ 8w  d#   \        P                  ! RRVRRV  R2.R^
R7       R#   \         d"   p\        P                  RT4        Rp?R# Rp?ii ; i)z:Launch an app on the TV via ADB.  Returns True on success.r
   zcom.netflix.ninja/.MainActivityr   zWcom.google.android.youtube.tv/com.google.android.apps.youtube.tv.activity.ShellActivityr   z.com.apple.atve.androidtv.appletv/.MainActivityr   z/com.disney.disneyplus/.ui.splash.LaunchActivityr   zFcom.amazon.amazonvideo.livingroom/com.amazon.ignition.IgnitionActivityr   z"org.droidtv.playtv/.PlayTvActivityr   r   r   z*am start -a android.intent.action.MAIN -n z --activity-clear-topT)r   rC   z
monkey -p z/ -c android.intent.category.LEANBACK_LAUNCHER 1z& -c android.intent.category.LAUNCHER 1zadb_launch_app failed: %sNF)	r;   r   rL   r   r   
returncoder&   rO   r   )r*  r2   r   launch_intents	componentrR   rS   s   &&     r   adb_launch_appr/    s   _F>'  *C*,\!R+-uBNB"&&w/	NNE4G	{Rghj*.<  tVW",WI5d e g.2B@A ||q tVW",WI5[ \ ^.2B@  /3s   AB: #AB: :C&C!!C&c          	      V    V ^8  d   QhR\         R\         R\        R\        R,          /# r   ro   rB   rV   r   Nr   )r   s   "r   r   r     s,      3 c   r   c                    RV 2pV'       d%   \         P                  ! V4      P                  4       MRp\        P                  P                  W4RR\         2RR/V R7      p\        P                  P                  V^
R7      p\         P                  ! VP                  4       4      #   \         d#   p\        P                  R	YT4        Rp?R# Rp?ii ; i)
z5Call HA Supervisor API.  Returns parsed JSON or None.http://supervisor/core/apiNAuthorizationBearer rh   application/jsonrm   rp   zha_api %s %s failed: %srN   dumpsrv   rs   rt   ru   SUPERVISOR_TOKENrw   loadsr#   r&   rO   rP   ro   rB   rV   rQ   rg   ry   rz   rS   s   &&&     r   ha_apir<    s    *4&1,0tzz$&&(dnn$$7+;*<!= 2  % 
 ~~%%c2%6zz$))+&& 		+V1=   B' BB' 'C2CCc                0    V ^8  d   QhR\         R\         /# )r   device_listr   list)r   s   "r   r   r     s      d t r   c                    \        RR4      pV'       g   . # . pV FP  pVR,          pW@9   g   K  VP                  R/ 4      P                  RV4      pVP                  WEVR,          34       KR  	  V# )zEReturn [(entity_id, friendly_name, state), ...] for given entity IDs.GETz/states	entity_id
attributesfriendly_namer  )r<  rL   append)r?  statesr   rS   eidnames   &     r   ha_get_device_statesrK    sm    E9%F	Fn55r*..DDMM3aj12	 
 Mr   c                0    V ^8  d   QhR\         R\        /# )r   rD  r   r`   )r   s   "r   r   r     s        r   c                ,    \        RRV  24      pV'       g   R# VP                  RR4      pV P                  R4      ^ ,          pVR8X  d   RMR	p\        R
RV RV 2RV /4       R#   \         d"   p\        P                  RT4        Rp?R# Rp?ii ; i)z3Toggle a HA switch/light.  Returns True on success.rC  /states/Fr  off.onturn_offturn_onrl   
/services/rG   rD  Tzha_switch_toggle failed: %sN)r<  rL   r  r&   rO   r   )rD  
state_datacurrentdomainservicerS   s   &     r   ha_switch_togglerY    s    EXi[#9:
..%0%a( '4*YvF81WI6i8PQ 115s   A' A
A' 'B2BBc                0    V ^8  d   QhR\         R\        /# )r   rD  duration)r   rD   )r   s   "r   r   r     s     7 7 7 7r   c                    \        RRV  24      pV'       g   R# VP                  RR4      pV P                  R4      ^ ,          pVR8X  d   RMRpVR8X  d   RMRp\        R	R
V RV 2RV /4       V^ 8  d   \        P
                  ! V4       \        R	R
V RV 2RV /4       R#   \         d"   p\        P                  RT4        Rp?R# Rp?ii ; i)uG  Toggle a HA switch for `duration` seconds, then restore original state.

The visible flicker is bounded by: toggle API latency + sleep + restore API
latency.  To keep it short, we use a 2s timeout on the individual calls and
skip the sleep when duration is very small — the API round-trip itself
provides enough visible gap.
rC  rN  Nr  rO  rP  rS  rR  rl   rT  rG   rD  zha_switch_flicker failed: %s)	r<  rL   r  _ha_api_fastr   r   r&   rO   r   )rD  r[  rU  rV  rW  
toggle_svcrestore_svcrS   s   &&      r   ha_switch_flickerr`    s    7EXi[#9:
..%0%a(")U"2Y

$+u$4j)Vz&:,?+yAYZa<JJx Vz&;-@;PYBZ[ 72A667s   B$ BB$ $C/CCc          	      V    V ^8  d   QhR\         R\         R\        R\        R,          /# r1  r   )r   s   "r   r   r     s,       C t td{ r   c                    RV 2pV'       d%   \         P                  ! V4      P                  4       MRp\        P                  P                  W4RR\         2RR/V R7      p\        P                  P                  V^R7      p\         P                  ! VP                  4       4      #   \         d#   p\        P                  R	YT4        Rp?R# Rp?ii ; i)
zFLike ha_api but with a short 2s timeout for time-sensitive operations.r3  Nr4  r5  rh   r6  rm   rp   z_ha_api_fast %s %s failed: %sr7  r;  s   &&&     r   r]  r]    s    *4&1,0tzz$&&(dnn$$7+;*<!= 2  % 
 ~~%%c1%5zz$))+&& 		16Cr=  c                <    V ^8  d   QhR\         R\        R\        /# )r   tv_ipbandwidth_kbpsr   )r   r~   rX   )r   s   "r   r   r   1  s!     - -# -c -T -r   c                "   T ;'       g    \        4       p Rp \        P                  ! RRRRVR.RR7       V^ 8:  d   R# \        P                  ! RRR	RVRR
RRRR.RRR7       \        P                  ! RRR	RVRRRRRRR.RRR7       \        P                  ! RRR	RVRRRRRRV R2RV R2.RRR7       \        P                  ! RRR	RVRRRRRRRRRR V  R!2RRRRR"R#R.RRR7       R+ FD  p\        P                  ! RRR	RVRRRRRRRRRR V  R!2RRRR$R"RRR%\        V4      R&R#R.RRR7       KF  	  \        P                  ! RRR	RVRRRRRR'RRRR V  R!2R#R.RRR7       R#   \         d"   p\
        P                  R(T4        R)p?R*# R)p?ii ; i),zDApply tc HTB throttle targeting the TV IP.  Returns True on success.end0tcqdiscdeldevrootTr   addhandlez1:htbdefault10)r   r   classparentclassidz1:10rate1000mbitz1:20kbitceilfilterprotocolr2   prior   u32matchdstz/320xffflowid6dport0xffff2zthrottle_apply failed: %sNF)i  r   )r(   r   r   r   r&   rO   r   )rd  re  rk  portrS   s   &&   r   throttle_applyr  1  s   !!Z\E
C)gueS&A&*	,QgueS&(Dy$0&*$	8 	gueS(D)vz;&*$	8 	gueS(D)v.1A/F>"2$ 79 '+$	8 	huc8T:fc5usmz3 &	*
 '+$	8 !DNND(E5#xz &#u#T5UG3-#T:sF#T7CIx$f. +/d< ! 	huc8T:fc5'4 'x9 '+$	8  /3s   %E" D!E" "F-F		Fc                <    V ^8  d   QhR\         R\        R\        /# )r   
sound_filer   r   r   rD   rX   )r   s   "r   r   r   a  s!      3   r   c                     \        RRRRRV/4       \        RRRRRRR	R
V  2/4       R#   \         d"   p\        P                  RT4        Rp?R# Rp?ii ; i)zFPlay a sound file on the Xiaomi Sound Pro speaker via HA media_player.rl   z!/services/media_player/volume_setrD  zmedia_player.sound_pro_1264volume_levelz!/services/media_player/play_mediamedia_content_typemusicmedia_content_idz!http://192.168.1.187:8888/sounds/Tzspeaker_play_sound failed: %sNF)r<  r&   rO   r   )r  r   rS   s   && r   speaker_play_soundr  a  sw    v:6F=
 	 	v:6 '"CJ< P=
 	
  3Q7s   '+ AAAc                $    V ^8  d   QhR\         /# r   rX   )r   s   "r   r   r   ~  s     	 	 	r   c                     Rp  \         P                  ! RRRRV R.RR7       R#   \         d"   p\        P	                  R	T4        R
p?R# R
p?ii ; i)z7Remove all tc throttle rules.  Returns True on success.rg  rh  ri  rj  rk  rl  Trm  zthrottle_remove failed: %sNF)r   r   r&   rO   r   )rk  rS   s     r   throttle_remover  ~  sO    
CgueS&A&*	, 0!4s   $ AAAz$/share/.ha_cache/.lounge_tokens.jsonYongWahzAhttps://www.youtube.com/api/lounge/pairing/get_lounge_token_batchz*https://www.youtube.com/api/lounge/bc/bindz'urn:dial-multiscreen-org:service:dial:1c                $    V ^8  d   QhR\         /# r   rE   )r   s   "r   r   r     s      T r   c                       \        \        4      ;_uu_ 4       p \        P                  ! V 4      uuR R R 4       #   + '       g   i     R # ; i  \        \        P
                  3 d    / u # i ; ir+   )r!   LOUNGE_TOKENS_PATHrN   loadFileNotFoundErrorJSONDecodeError)r   s    r   _lounge_load_tokensr    sM    $%%99Q< &%%%t334 	s+   A :
A A	A A A.-A.c                (    V ^8  d   QhR\         RR/# )r   tokensr   Nr  )r   s   "r   r   r     s     ( ( ( (r   c                 X   \         P                  ! \         P                  P                  \        4      R R7       \        R,           p\        VR4      ;_uu_ 4       p\        P                  ! W^R7       RRR4       \         P                  ! V\        4       R#   + '       g   i     L-; i)T)exist_okz.tmpw)indentN)	osmakedirsrB   dirnamer  r!   rN   dumpr   )r  tmpr   s   &  r   _lounge_save_tokensr    s_    KK 23dC
v
%C	c31		&A& 
JJs&' 
s   BB)	c                h    V ^8  d   QhR\         R\        \        \        \        3,          ,          /# r   rC   r   )rD   rA  r  r   )r   s   "r   r   r     s'      U T%S/-B r   c                   R\          R2P                  4       p\        P                  ! \        P                  \        P                  4      pVP                  \        P                  \        P                  ^4       VP                  V 4       . p VP                  VR	4       \        P                  ! 4       V ,           p\        P                  ! 4       V8  d    VP                  R4      w  rVRpTP                  RR7      P                  4        FQ  pTP                  4       P!                  R4      '       g   K*  TP#                  R^4      ^,          P%                  4       p M	  T'       g   K  TP'                  T^ ,          T34       K  VP)                  4        V#   \        P                   d     K*  i ; i  TP)                  4        i ; i)
zJSSDP M-SEARCH for DIAL services on LAN. Returns [(ip, location_url), ...].zRM-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: z

r   Nignorer   z	location:r9   r   )DIAL_SSDP_STrv   r   r   r   r   
SOL_SOCKETSO_BROADCASTr   r   r   r   rC   rx   r  r   
startswithr  r$   rG  r   )	rC   msgr   founddeadlinerV   r   locr  s	   &        r   _ssdp_find_dialr    sa   	 n 		 fh  ==):):;DOOF%%v':':A>OOGEC2399;(iikH$!]]40
 C84??A::<**;77**S!,Q/557C B sd1gs^,

L >>  	

s>   AF6 F +AF6 70F6 ,F6 F3/F6 2F33F6 6Gc                >    V ^8  d   QhR\         R\         R,          /# )r   location_urlr   Nr   )r   s   "r   r   r     s      S S4Z r   c                     \         P                  ! V ^R7      pVP                  4        VP                  P                  R4      #   \         d"   p\
        P                  RY4        Rp?R# Rp?ii ; i)zDFetch DIAL device description and return the Application-URL header.rp   zApplication-URLz!DIAL desc fetch failed for %s: %sN)rK   rL   rM   rn   r&   rO   r   )r  rR   rS   s   &  r   _dial_get_apps_urlr    sY    LLq1	yy}}.// 7Is   AA A1A,,A1c                >    V ^8  d   QhR\         R\         R,          /# )r   apps_urlr   Nr   )r   s   "r   r   r     s      # #* r   c                   V P                  R4      R,           p \        P                  ! V^R7      pVP                  ^8w  d   R# \        P
                  ! RRVP                  ^R7      p\        P                  ! V4      pVP                  R4      pVe-   VP                  '       d   VP                  P                  4       # R#   \         d"   p\        P                  R	T4        Rp?R# Rp?ii ; i)
zGET <apps_url>/YouTube and parse <screenId> from the response.

YouTube must be running in the foreground for screen_id to be exposed.
Returns None if YouTube isn't running or DIAL doesn't expose YouTube.rG   z/YouTuberp   Nz\sxmlns="[^"]+"r   )countz.//screenIdzDIAL YouTube probe failed: %s)r  rK   rL   r\   r   subr   _ET
fromstringfindr$   r&   rO   r   )r  rQ   rR   rg   rl  sid_elrS   s   &      r   _dial_get_youtube_screen_idr    s    
 //#

+C8LLa(==Cvv("affA>~~d#=)&+++;;$$&&   83Q778s$   )C A C &C C.C))C.philipsc                J    V ^8  d   QhR\         R\         R\        R,          /# )r   rd  screen_namer   Nr   )r   s   "r   r   r     s&     - - -# -dUYk -r   c                   Rp\        RR7       F"  w  r4W08X  g   K  \        V4      pV'       g   K"   M	  V'       g   RV  R2p\        V4      pV'       g   \        P	                  RV 4       R#  \
        P                  ! \        RV/^
R7      pVP                  4        VP                  4       P                  R	. 4      pV'       g   R# V^ ,          P                  R
4      pV'       g   R#  RTRTRT R\        \        P                  ! 4       4      /p
\        4       pYT&   \        T4       \        P!                  RYTR,          4       T
#   \         d"   p	\        P	                  RT	4        Rp	?	R# Rp	?	ii ; i)u  Discover the TV's DIAL service, fetch the YouTube screen_id while the
YouTube app is foreground, and exchange it for a long-lived lounge_token.

Persists the result to LOUNGE_TOKENS_PATH under `screen_name`. Silent on TV
— no code displayed, no popup. Returns the token entry dict, or None if
pairing failed (e.g. YouTube not running). Must be called when YouTube is
actually visible on the TV, otherwise DIAL returns 404 for /apps/YouTube.N      @rp   r>   z
:8008/appszElounge_pair_via_dial: no screen_id (YouTube must be foreground on %s)
screen_ids)rV   rC   screensloungeTokenz"lounge pairing exchange failed: %s	screen_idlounge_tokenrd  	paired_atz*lounge paired: screen=%s tv=%s token=%s...N   N)r  r  r  rO   r   rK   r[   LOUNGE_PAIRING_URLrM   rN   rL   r&   r~   r   r  r  r   )rd  r  r  r2   r  r  rR   r  r  rS   entryr  s   &&          r   lounge_pair_via_dialr    sN    H"3/;)#.Hx	 0
 UG:.+H5I[]bcMM, ,i8"$& 	
&&(,,y"-qz~~m4  	YS%	E !"F;HH9;|\^O_`L  8!<s   /AE
 E
 
E6E11E6c          	      V    V ^8  d   QhR\         R\         R\         R\        R,          /# )r   pairing_coder  rd  r   Nr   )r   s   "r   r   r     s.     . . .# .!$.15.r   c                   \         P                  ! RRT ;'       g    R4      p\        V4      ^8w  d"   \        P	                  R\        V4      4       R#  \
        P                  ! RRV/RRR	R
/^
R7      pVP                  ^8w  d5   \        P	                  RVP                  VP                  R,          4       R# VP                  4       pTP                  R4      ;'       g    / pTP                  R4      pTP                  R4      p	T'       d	   T	'       g0   \        P	                  R\        TP                  4       4      4       R# RTRT	RTR\        \        P                  ! 4       4      RR/p
\!        4       pYT&   \#        T4       \        P%                  RYR,          4       T
#   \         d"   p\        P	                  RT4        Rp?R# Rp?ii ; i)u  Exchange a 12-digit YouTube TV pairing code for a long-lived lounge_token.

Used when DIAL discovery doesn't expose YouTube (modern Google TVs / Cast V2
devices). User reads the code from TV → Settings → Watch on TV → "Link with
TV code", then passes it here. Code is single-use and expires in ~5 minutes,
but the resulting lounge_token is good for months/years.

Accepts code with or without dashes (e.g. "123-456-789-012" or "123456789012").
Returns the persisted token entry, or None on failure.z[^0-9]r   z>lounge_pair_with_code: bad code length %d (expected 12 digits)Nz5https://www.youtube.com/api/lounge/pairing/get_screenr  zX-YouTube-Client-Namer   zX-YouTube-Client-Versionz2.0)rV   rn   rC   z!lounge get_screen returned %d: %s:N   Nz(lounge_pair_with_code request failed: %sscreenscreenIdr  z$lounge get_screen missing fields: %sr  r  rd  r  ro   codez-lounge paired via code: screen=%s token=%s...r  )r   r  r  rO   r   rK   r[   r\   r   rN   r&   rL   rA  keysr~   r   r  r  r   )r  r  rd  r  rR   rV   rS   r  r  r  r  r  s   &&&         r   lounge_pair_with_coder    s    66)R!3!34D
4yBTVYZ^V_`MMC $',c3MuU	
 ==CKK;Q]]AFFSWLYvvx XXh%%2F

:&I::m,L,:D<OPYS%&E !"F;HH<kXZK[\L)  >Bs   A#F& :F& &G1GGc                >    V ^8  d   QhR\         R\        R,          /# r   r  r   Nr   )r   s   "r   r   r   M  s     2 2# 2dTk 2r   c                4    \        4       P                  V 4      # )z"Return cached token entry or None.)r  rL   r  s   &r   lounge_get_tokenr  M  s     $$[11r   c                (    V ^8  d   QhR\         RR/# r  r   )r   s   "r   r   r   R  s        C    r   c                 T    \        4       pVP                  V R 4       \        V4       R # r+   )r  popr  )r  r  s   & r   lounge_clear_tokenr  R  s      "F
JJ{D!r   c                h    V ^8  d   QhR\         R\        \        \         \        3,          ,          /# )r   r   r   )r   rA  r  object)r   s   "r   r   r   X  s(     " " "U3;-?(@ "r   c                   . p^ pV\        V 4      8  Ed   V P                  RV4      pV^ 8  d    V#  \        WV P                  4       4      pY^,           T^,           T,            pT^,           T,           p \
        P                  ! T4      pT F  p\        T\        4      '       d.   \        T4      ^8  d   \        T^,          \        4      '       g   KH  T^,          pT'       g   K[  T^ ,          p	\        T4      ^8X  d
   T^,          MTR,          p
TP                  Y34       K  	  EK*  V#   \         d    T^,           p EKD  i ; i  \
        P                   d     EKa  i ; i)aZ  Parse YouTube Lounge /bind chunked response.

Format: lines alternate between length count and JSON payload. Payload is
an array of [event_id, [event_type, ...event_data]] entries. We flatten
those to (event_type, event_data) tuples, where event_data is whatever
follows event_type in the inner array (single value if one item, list if
multiple).
:   NN)r  r  r~   r$   
ValueErrorrN   r:  r  
isinstancerA  rG  )r   outr   nlnchunkarrr  inneretypeedatas   &          r   _lounge_parse_chunkedr  X  sB    C	A
c$i-YYtQ6* J)	D2J$$&'A !VR!VaZ(FQJ	**U#C Eud++E
aJuUVxY]D^D^!HE!HE #E
aE!HU2YEJJ~&  J%  	QA	 ## 		s#   D1 8E 1E
	E
E'&E'c                   *  a  ] tR tRt o RtRV 3R lR lltV 3R lR ltV 3R lR ltR V 3R	 lR
 lltR V 3R lR llt	R!V 3R lR llt
R"V 3R lR lltV 3R lR ltR#V 3R lR lltV 3R lR ltV 3R lR ltV 3R lR ltR$V 3R lR lltRtV tR# )%LoungeSessioni}  u%  Minimal sync YouTube Lounge client. Connect → send commands → optionally
poll for state. Not async — designed to run inside a sandman action thread.

Does NOT keep a long-poll open continuously; we sip state on demand around
each setPlaylist call (enough for cycle-on-failure detection).c                    < V ^8  d   QhRS[ /# r   r  r   )r   __classdict__s   "r   r   LoungeSession.__annotate__  s     &' &'C &'r   c                B   Wn         \        V4      pV'       g   \        R V R24      hVR,          V n        VR,          V n        VP                  R4      pV'       g;   R\        P                  ! ^4      ,          pW2R&   \        4       pW$V&   \        V4       W0n
        \        P                  ! 4       V n        RV n        RV n        RV n        / V n        \        P$                  ! RR	4      V n        R
V n        RV n        RV n        \.        P0                  ! 4       V n        R# )zno lounge token for screen 'z!'; run lounge_pair_via_dial firstr  r  	device_idz%032xNr   i'  i         F)r  r  RuntimeErrorr  r  rL   _randomgetrandbitsr  r  r  rK   r   sessionsid
gsessionidaidnow_playingrandint_ridlast_event_atcontent_video_id	ad_active	threadingRLock_lock)selfr  r  r  r  s   &&   r   __init__LoungeSession.__init__  s    & -!=k]Jklmm{+!.1 IIk*	'"5"5c"::I!*+(*F"';'"'')OOE51	 ! $ __&
r   c                    < V ^8  d   QhRS[ /# r   r   )r   r  s   "r   r   r    s      3 r   c                ^    V ;P                   ^,          un         \        V P                   4      # )r  )r	  r   r  s   &r   	_next_ridLoungeSession._next_rid  s    		Q	499~r   c                $   < V ^8  d   QhRS[ RR/# )r   eventsr   Nr@  )r   r  s   "r   r   r    s     - -d -t -r   c                   V'       d   \         P                   ! 4       V n        V EF-  w  r#VR 8X  d1   \        V\        4      '       d   V'       d
   V^ ,          MRV n        K=  VR8X  d	   W0n        KL  VR8X  d|   \        V\        4      '       df   R F   pWC9   g   K  W4,          V P                  V&   K"  	  V P                  '       d,   VP                  R4      V P                  8X  d
   RV n
        K  K  K  VR8X  d@   \        V\        4      '       d*   R F   pWC9   g   K  W4,          V P                  V&   K"  	  EK  VR8X  d   \        V\        4      '       d   VP                  R4      pV'       d   V P                  P                  RV4       VP                  R4      pV'       d=   RV P                  9  d)   VP                  R	4      ^ ,          V P                  R&   EK  EK  EK  VR
8X  d_   \        V\        4      '       dI   VP                  R4      pV'       d-   Wpn        RV n
        V P                  P                  RV4       EK   EK#  VR8X  g   EK-  EK0  	  R# )cNS
nowPlayingvideoIdFonStateChangeplaylistModifiedvideoIdsr  onAdStateChangecontentVideoIdTnoop)r  currentTimer  r[  listId)r%  r  r[  )r   r
  r  rA  r  r  rE   r  r  rL   r  
setdefaultr  )r  r  r  r  kvidvidscvids   &&      r   _absorbLoungeSession._absorb  s   !%D"LE|
5$ 7 7',58$#"',&:eT+B+BRAz.3h((+ S
 )))!IIi0D4I4II%*DN J * /)j.E.E=Az.3h((+ > ,,E41H1H ii	*$$//	3?yy,IT-=-==26**S/!2DD$$Y/ >4++
5$0G0G
 yy!12,0)%)DN $$//	4@  &U #r   c                &   < V ^8  d   QhRS[ RS[/# r  rD   rX   )r   r  s   "r   r   r    s     1 1 1$ 1r   c                P   V P                   ;_uu_ 4         V P                  P                  4        \        P
                  ! 4       V n        RV n        RV n        RV n        RV n	        V P                  VR7      uuRRR4       #   \         d     L_i ; i  + '       g   i     R# ; i)a  Tear down a dead session and re-bind on the same lounge_token.
Used to recover from `LoungeSessionDead` without re-pairing via DIAL.
Issues a fresh SID/gsessionid; existing long-polls on the old SID will
return 400 once and unwind cleanly. Returns True on success.Nr   r  rp   )r  r  r   r&   rK   r   r  r  r  r
  r   r  rC   s   &&r   	reconnectLoungeSession.reconnect  s    
 ZZZ""$ $++-DLDH"DODH!$D<<<0 Z   ZZs.   BBABBBBBB%	c                &   < V ^8  d   QhRS[ RS[/# r  r/  )r   r  s   "r   r   r    s     D Du D Dr   c                   RV P                  4       RRRRRRR\        R	R
RV P                  RV P                  /pRV P                  RRRR/pV P                  P                  \        VRW1R7      pVP                  R8X  d   \        R4      hVP                  4        V P                  \        VP                  4      4       V P                  RJ;'       d    V P                  RJ# )zDOpen a session. Returns True on success. Captures now-playing state.RIDVER8CVERr   deviceREMOTE_CONTROLrJ  appzyoutube-desktoploungeIdTokenidX-YouTube-LoungeId-Tokenrh   !application/x-www-form-urlencodedOriginhttps://www.youtube.comzcount=0paramsrV   rn   rC     lounge token rejected (401)N)r  LOUNGE_DEVICE_NAMEr  r  r  r[   LOUNGE_BIND_URLr\   LoungeAuthErrorrM   r,  r  r   r  r  r  rC   rD  rn   rR   s   &&   r   r   LoungeSession.connect  s     4>>#3C&&$T..$..	
 '(9(9?/

 LLof#,g  P==C!"?@@	*16623xxt#CCt(CCr   Nc                @   < V ^8  d   QhRS[ RS[R,          RS[RS[/# )r   commandrD  NrC   r   rW   )r   r  s   "r   r   r  	  s.       TD[ % Z^ r   c                   T;'       g    / pR V P                  4       RRRRRV P                  RV P                  RV P                  /pRRR	R
RV/pVP	                  4        F  w  rg\        V4      VRV 2&   K  	  RV P                  RRRR/pV P                  ;_uu_ 4        V P                  P                  \        VWXVR7      p	V	P                  R8X  d   \        R4      h\        V	4       V	P                  4        V	P                  '       d&    V P                  \!        V	P                  4      4       RRR4       R#   \"         d     Li ; i  + '       g   i     R# ; i)r6  r7  r8  r9  r   SIDr  r=  r  ofsr   req0__screq0_r?  rh   r@  rA  rB  rC  rE  rF  NT)r  r  r  r  itemsr   r  r  r[   rH  r\   rI  _lounge_check_deadrM   r   r,  r  r&   )
r  rM  rD  rC   
url_paramsformr(  vrn   rR   s
   &&&&      r   _commandLoungeSession._command	  sJ   24>>#3C488$//T..

 eS*g>LLNDA #AD5 # '(9(9?/

 ZZZ!!/*'+g " OA}}#%&CDDq! vvvLL!6qvv!>?   !  Z s+   A-E$D;;E	EE		EE	c                ,   < V ^8  d   QhRS[ RS[RS[/# )r   video_idcurrent_timer   r  )r   r  s   "r   r   r  *  s"      S   r   c                P    V P                  R RVR\        V4      RRRRRRR	R/4      # )
setPlaylistr  r%  currentIndexz-1	audioOnlyfalserD  r   playerParamsrX  r   )r  r[  r\  s   &&&r   set_playlistLoungeSession.set_playlist*  s=    }}]x3|,DbB-
  	r   c                    < V ^8  d   QhRS[ /# r   r  )r   r  s   "r   r   r  4  s     & &T &r   c                <   V P                   p V P                  R4       \        ^4       F)  p T P                  RR7       T P                   T8  g   K)   M	  \        T P                  4      #   \         d   p\        P	                  RT4        T P                  4        T P                   p Rp?L  \         d;   p\        P	                  RT4       \        T P                  4      u Rp?u Rp?# Rp?ii ; iRp?i\         d     Li ; i  \         d      K  i ; i)um  Best-effort current-state read. Asks the receiver to broadcast and
polls briefly, but this TV's YouTube often doesn't echo state to
getNowPlaying — so we fall back to whatever connect() captured (the
receiver's snapshot at session-start, which is usually the most-recent
real video and our best resume target).

Returns {} only if connect() also captured nothing.getNowPlayingu(   request_now_playing: %s — reconnectingz!request_now_playing reconnect: %sN       @rp   )r
  rX  LoungeSessionDeadrO   r   r2  r&   rE   r  r   
poll_state)r  prerS   e2r   s   &    r   request_now_playing!LoungeSession.request_now_playing4  s       	MM/* qA, !!C'  D$$%%% ! 	.KKBAF. (( .?DD,,---.  		 % sd   A, D,D7C7B//C4:*C/$C4%C7)D/C44C77DDDDDc                2   < V ^8  d   QhRS[ RS[RS[RS[/# )r   r[  r\  verify_timeoutr   r  )r   r  s   "r   r   r  S  s1     B Bc B B.3B>BBr   c                    V P                   p V P                  W4       \        P                  ! 4       T,           p\        P                  ! 4       T8  dW    T P                  \        R\        RT\        P                  ! 4       ,
          4      4      R7       T P                   T8  g   Kn  R#  T P                  Y4       \        P                  ! 4       T,           p\        P                  ! 4       T8  dW    T P                  \        R\        RT\        P                  ! 4       ,
          4      4      R7       T P                   T8  g   Kn  R# R#   \         d    h \         d   p\        P                  RT4        T P                  4        T P                  Y4       T P                   p Rp?EL  \         d    h \         d&   p\        P                  RT4        Rp? Rp?ELRp?ii ; iRp?i\         d"   p\        P                  RT4        Rp?ELRp?ii ; i  \         d     EK  i ; i  \         d    h \         d   p\        P                  R	T4        T P                  4        T P                  Y4       T P                   p Rp?EL  \         d    h \         d&   p\        P                  R
T4        Rp? Rp?ELRp?ii ; iRp?i\         d"   p\        P                  RT4        Rp?EL9Rp?ii ; i  \         d     R# i ; i)u  setPlaylist with belt-and-suspenders delivery. We can't reliably
"verify" the command because this TV's YouTube receiver accepts and
renders setPlaylist but does NOT always echo state back through the
Lounge channel — so a successful command still looks "silent" to us.

Strategy: send once, poll briefly for any fresh receiver event. If we
got fresh activity, assume warm and return True. If silent, the link
MIGHT be cold (or just non-echoing); send a second time as a warmup —
empirically the second command often sticks when the first didn't.

Returns True if at least one attempt produced fresh events (link
confirmed warm), False if both attempts were silent. Callers should
treat False as "link probably cold, but the command may still have
landed" — log it but don't abort.u1   set_playlist_verified send 1: %s — reconnectingz,set_playlist_verified reconnect+resend 1: %sNz set_playlist_verified send 1: %sr  g?rp   Tu1   set_playlist_verified send 2: %s — reconnectingz,set_playlist_verified reconnect+resend 2: %sz set_playlist_verified send 2: %sF)r
  rd  rI  rj  rO   r   r2  r&   r   rk  r   r   )r  r[  r\  rq  rl  rS   rm  r  s   &&&&    r   set_playlist_verified#LoungeSession.set_playlist_verifiedS  s_       	?h5 99;/iikH$CS(TYY[:P1Q(RS !!C'	?h5 99;/iikH$CS(TYY[:P1Q(RS !!C'_  	  		PKKKQOP !!(9(("  PJBOOP 	?KK:A>>	? %   	  		PKKKQOP !!(9(("  PJBOOP 	?KK:A>>	? %  	s   E A H ,H. 4A K> H H!G)8-F++G& G&G!G)!G&&G))H6H7HHH+*H+.K;K;K-JK	#K	$K:KK		KK;K;K66K;>LLc                    < V ^8  d   QhRS[ /# r   r  )r   r  s   "r   r   r    s     % %d %r   c                $    V P                  R 4      # )playrX  r  s   &r   rw  LoungeSession.play  s    }}V$$r   c                    < V ^8  d   QhRS[ /# r   r  )r   r  s   "r   r   r    s     & &t &r   c                $    V P                  R 4      # )pauserx  r  s   &r   r|  LoungeSession.pause  s    }}W%%r   c                &   < V ^8  d   QhRS[ RS[/# )r   r\  r   r/  )r   r  s   "r   r   r    s     G GE Gd Gr   c                <    V P                  R R\        V4      /4      # )seekTonewTimerc  )r  r\  s   &&r   seek_toLoungeSession.seek_to  s    }}X	3|3D'EFFr   c                &   < V ^8  d   QhRS[ RS[/# r  )rD   rE   )r   r  s   "r   r   r    s     * *% *$ *r   c                H   RRRV P                   RRRV P                  RRR	V P                  R
RRRRV P                  /	pRV P                  RR/pV P                  ;_uu_ 4         V P
                  P                  \        VW1R7      p\        V4       VP                  '       d%   V P                  \        VP                  4      4       \        T P                   4      uuRRR4       #   \        P                  \        P                  3 d     LFi ; i  + '       g   i     R# ; i)zOne-shot long-poll for state events. Updates now_playing in place
and returns a copy. Returns whatever's accumulated even on timeout.r6  rpcrO  CIr   AIDTYPExmlhttpr  r7  r8  r9  r   r=  r?  rA  rB  )rD  rn   rC   N)r  r  r  r  r  r  rL   rH  rT  r   r,  r  rK   TimeoutConnectionErrorrE   r  rJ  s   &&   r   rk  LoungeSession.poll_state  s    5488#488I$//3CT..

 '(9(9/
 ZZZLL$$_V-4 % G"1%666LL!6qvv!>? (() Z $$h&>&>?  ZZs1   !D#A#C%D%%D
DDDD!	)r  r	  r  r  r  r  r  r
  r  r  r  r  r  r  r  g      $@)Ng       @)r  )r  g      @r  )__name__
__module____qualname____firstlineno____doc__r  r  r,  r2  r   rX  rd  rn  rs  rw  r|  r  rk  __static_attributes____classdictcell__r  s   @r   r  r  }  s     F&' &'P - -^1 1"D D2 B & &>B BH% %& &G G* * *r   r  c                       ] tR tRtRtRtR# )rI  i  z<Lounge token rejected (401). Caller should re-pair via DIAL.r.   Nr  r  r  r  r  r  r.   r   r   rI  rI    s    Fr   rI  c                       ] tR tRtRtRtR# )rj  i  u   Lounge SID/gsessionid invalidated (400 Unknown SID). The lounge_token
is still valid — caller should re-bind on the same token rather than
re-pair. Distinct from LoungeAuthError so the keeper/campaign can
transparently recover without DIAL.r.   Nr  r.   r   r   rj  rj    s    + 	r   rj  c                    V ^8  d   QhRR/# r   r   Nr.   )r   s   "r   r   r     s     L LT Lr   c                x    V P                   R8X  d)   RV P                  ;'       g    R9   d   \        R4      hR# R# )u  Raise LoungeSessionDead if the response body indicates an invalidated
SID. YouTube returns 400 with body 'Unknown SID' when the SID has expired,
been evicted, or rotated — at that point every command on this session
will keep failing until we mint a fresh SID via re-bind.rZ   zUnknown SIDr   z(lounge SID invalidated (400 Unknown SID)N)r\   r   rj  )rR   s   &r   rT  rT    s6    
 	}}!&&,,B ? JKK !@r   c                      a  ] tR tRt o RtRt]P                  ! 4       tRV 3R lR llt	]
RV 3R lR ll4       tV 3R lR	 ltV 3R
 lR ltRV 3R lR lltV 3R lR ltV 3R lR ltV 3R ltRtV tR# )LoungeKeeperi  u  Module-level singleton that holds a long-lived LoungeSession and runs
a background poll loop. The point: this TV's YouTube only renders Lounge
setPlaylist commands while it considers a controller actively present.
Connect-fire-disconnect (the previous pattern) lets the receiver mark us
as gone within minutes, after which our setPlaylist commands return 200
but render nothing on screen.

A continuous long-poll keeps the TV's controller list populated with us,
so commands stay live. Reconnects with backoff if the session dies.
Campaign code grabs `.session` (the live LoungeSession) and sends through
it directly — the keeper's RLock serializes them safely.Nc                    < V ^8  d   QhRS[ /# r  r   )r   r  s   "r   r   LoungeKeeper.__annotate__  s     ( (C (r   c                    Wn         R V n        \        P                  ! 4       V n        R V n        \        P                  ! 4       V n        R # r+   )r  r  r  Event_stop_thread_ready)r  r  s   &&r   r  LoungeKeeper.__init__  s2    &-1__&
04oo'r   c                $   < V ^8  d   QhRS[ RR/# )r   r  r   r  r   )r   r  s   "r   r   r    s     ! !c !. !r   c                    V P                   ;_uu_ 4        V P                  f   V ! V4      pVP                  4        W n        V P                  uuR R R 4       #   + '       g   i     R # ; ir+   )_instance_lock	_instancestart)clsr  insts   && r   rL   LoungeKeeper.get  sF    }}$;'

 $==  s   8AA)	c                   < V ^8  d   QhRR/# r  r.   )r   r  s   "r   r   r    s      t r   c                @   V P                   '       d#   V P                   P                  4       '       d   R # V P                  P                  4        \        P
                  ! V P                  RRV P                   2R7      V n         V P                   P                  4        R # )NTzLoungeKeeper-)r   daemonrJ  )	r  is_aliver  clearr  Thread_runr  r  r  s   &r   r  LoungeKeeper.start  sl    <<<DLL1133

 ''tyy0=d>N>N=O.PRr   c                   < V ^8  d   QhRR/# r  r.   )r   r  s   "r   r   r    s      d r   c                :    V P                   P                  4        R # r+   )r  setr  s   &r   stopLoungeKeeper.stop  s    

r   c                &   < V ^8  d   QhRS[ RS[/# r  r/  )r   r  s   "r   r   r    s     1 1% 14 1r   c                :    V P                   P                  VR 7      # )rp   )r  waitr1  s   &&r   
wait_readyLoungeKeeper.wait_ready  s    {{00r   c                   < V ^8  d   QhRR/# )r   r   zLoungeSession | Noner.   )r   r  s   "r   r   r    s     K K3 Kr   c                z    V P                   '       d)   V P                   P                  '       d   V P                   # R# )zReturn the live session if connected, else None. Caller should
check before sending and fall back to a fresh session if absent.N)r  r  r  s   &r   get_sessionLoungeKeeper.get_session  s*      $|||0@0@0@t||JdJr   c                   < V ^8  d   QhRR/# r  r.   )r   r  s   "r   r   r    s     #/ #/d #/r   c                N   R pV P                   P                  4       '       Eg	    \        V P                  4      '       g3   \        P                  R4       V P                   P                  ^<4       Ko  \        V P                  R7      pVP                  4        W n	        V P                  P                  4        \        P                  RVP                  4       R pV P                   P                  4       '       g0   VP                  RR7       V P                   P                  R4       KO  EK)  R#   \         d=    \        P                  R4       RT n	        T P                   P                  ^x4        EKq  \          dG   p\        P                  R	T4       RT n	        R pT P                   P                  ^4        Rp?EK  Rp?i\"         dX   p\        P                  R
Y14       RT n	        T P                   P                  T4       \%        T^,          ^<4      p Rp?EK  Rp?ii ; i)ri  z'LoungeKeeper: no token cached; sleepingr  z LoungeKeeper: connected (sid=%s)g      >@rp   g?u.   LoungeKeeper: token rejected — needs re-pairNz(LoungeKeeper: %s; re-binding immediatelyz'LoungeKeeper: %s; reconnecting in %.0fs)r  is_setr  r  rO   r   r  r  r   r  r  r  r  rk  rI  r   rj  r&   r   )r  backoffsessrS   s   &   r   r  LoungeKeeper._run  s   **##%% /'(8(899HHFGJJOOB'$1A1AB#!;TXXF **++--OODO1JJOOC( .# &( # %LM#

$$$ #CQG#

"" /EqR#

(gk2.	/sE   AD. 2BD. :/D. .AH$4H$=H$>:F??H$H$AHH$c                $   < V ^8  d   Qh/ R;R&   # )r   zLoungeKeeper | Noner  r.   )r   r  s   "r   r   r    s      %+ r   )r  r  r  r  r  r  r  )r  r  r  r  r  r  r  Lockr  r  classmethodrL   r  r  r  r  r  __annotate_func__r  r  r  s   @r   r  r    s}     
B (,I^^%N( ( ! ! !  1 1K K
#/ #/g  r   r  c                0    V ^8  d   QhR\         R\        /# )r   r  r   )r   r  )r   s   "r   r   r   -  s     ) )S ) )r   c                ,    \         P                  V 4      # )u   Start (or return) the background keepalive that holds the TV's Lounge
subscription warm. Idempotent — safe to call multiple times.)r  rL   r  s   &r   lounge_keeper_startr  -  s     K((r   r  c                v    V ^8  d   QhR\         R\        R\         R\         R\        \        \        3,          /# )r   r[  r\  r  rd  r   )r   rD   r  rX   rE   )r   s   "r   r   r   3  s;      c  14+.;@t;Lr   c                    \        VR7      pVP                  4       '       g   R/ 3# VP                  W4       RVP                  RR7      3#   \         d    \
        P                  RT4       \        Y2R7      '       d    \        TR7      pTP                  4       '       g   R/ 3u # TP                  Y4       RTP                  RR7      3u #   \         d!   p\
        P                  RT4        R	p?MR	p?ii ; iR/ 3u # \        \        P                  3 d%   p\
        P                  R
T4       R/ 3u R	p?# R	p?ii ; i)a  Convenience wrapper: connect, send setPlaylist, return (ok, now_playing).
On 401 (rotated token), re-pair via DIAL once and retry. Used by the
sandman ad-break action; logs to standard tv_control logger so the
Telegram bot's /lounge command can surface activity.r  FTr  rp   u3   lounge token rotated for %s — re-pairing via DIAL)rd  r  z%lounge retry after re-pair failed: %sNzlounge_set_playlist failed: %s)r  r   rd  rk  rI  rO   r   r  r&   r   rK   RequestException)r[  r\  r  rd  r  rS   s   &&&&  r   lounge_set_playlist_with_repairr  3  s   5||~~"9(1T__S_111 I;WeEEH$=||~~ "9$!!(9T__S_999 HCQGGHby(334 4a8bysd   %A $A 3E%C&E)$CEC;C61E6C;;EEED=7E=Er+   )   N)g      @)g      @N)z/tmp/tv_screen.pngN)Ni  )g333333?)zknock_2.mp3zcough_single.mp3zthroat_final2.mp3ztongue_click.mp3zcustom_20to22.mp3zexhale_loud.mp3zexhale_soft.mp3r  r  )dr  concurrent.futuresr   rN   loggingr  r   r   r   r   r  r   urllib.requestrs   rK   requests.authr   urllib3disable_warnings
exceptionsInsecureRequestWarning	getLoggerrO   r%   r,   r-   r?   rr   rq   r:   	APP_NAMESr9  r!   _f_liner  r$   r  r&   r"   r(   r/   r6   r;   r@   rT   r]   rb   r{   r   r   r   r   r   r   r   r   r   r   r   r   r(  r/  r<  rK  rY  r`  r]  r  r  SPEAKER_SOUNDSr  r  rG  r  rH  r  randomr  xml.etree.ElementTreeetreeElementTreer  r  r  r  r  r  r  r  r  r  r  r  rI  rj  rT  r  r  r  r.   r   r   <module>r     s      	 	  
      (    ++BB C% 	P	<+   $Y'I)!?&*F#V +JY)i	&  		&	'	'2E 344#(;;=#6#6sA#>q#A   
( .
5"
%
E
	5, 

5?.=K5t6&	&G TD( 74,-`$	& <  X >8  # #(D* ',	 -` AJ',.b2
 "J~* ~*B
	i 	
		 	LV/ V/r) JM7@16 [' 
(	'	'  		s6   $J8 4J$+J$>J8 $J5	/J8 5J8 8KK