
    5i             
          U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlmZ ddlmZ ddlZddlmZmZmZmZmZmZmZmZ ddlmZmZmZmZ ddlm Z  dZ! e" e# e$d	      j                                     Z%e! d
e% Z&e! de% Z'e! de% Z(dZ)dZ*dZ+dZ,dZ-dZ.dZ/dZ0dZ1dZ2dZ3dZ4dZ5dZ6dZ7dZ8dZ9ddd d!dd"d#d!d"d$d%d!d$d&d'd!d&d(d)d!d(dd*d!gZ:d+e;fd,Z<d-e;fd.Z=d/e#d0e#d+e>fd1Z?d2e>d+e"fd3Z@d4e>d5e>d+e"fd6ZA ej                  d7      ZCeCj                  j                          eCj                  ej                         d8eC_H         ej                  d9d:;      ZJ ee+d<d=>      ZKeKj                  eJ       eCj                  eK        ej                         ZOeOj                  eJ       eCj                  eO       d+ePfd?ZQdd@e"dAe"d4e>fdBZRdaSe#dz  eTdC<   daUe"dz  eTdD<   daVe"dz  eTdE<   daWe"dz  eTdF<   daXe"dz  eTdG<   daYe"dz  eTdH<   daZe#eTdI<   da[e#dz  eTdJ<   ddKePd+e"dz  fdLZ\d+e"dz  fdMZ]dN Z^da_ddOe"dPe>d+e;dz  fdQZ`ddOe"dRe;dPe>d+ePfdSZadTe"d+ePfdUZbd+e#dz  fdVZcdWe#fdXZddYePfdZZed+e#dz  fd[Zfd\e#fd]Zgd^ed+ePfd_Zhd+e#dz  fd`Zid+ePfdaZjdbe"d+ePfdcZkd+ePfddZld+emfdeZnd+e"dz  fdfZodge"d+e"fdhZpd^ediej                  fdjZrd^ediej                  fdkZsd^ediej                  fdlZtd^ediej                  fdmZud^ediej                  fdnZvd^ediej                  fdoZwd^ediej                  fdpZxd^ediej                  fdqZyd^ediej                  fdrZzd^ediej                  fdsZ{d^ediej                  fdtZ|d^ediej                  fduZ}d^ediej                  fdvZ~d^ediej                  fdwZd^ediej                  fdxZdyZg dzZd+e;fd{Zdd|e"dOe"dRe;d+e;dz  fd}Zd+efd~Zdgg dddgg ddgdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZdadadadZd Zd ZdDe"de#fdZd Zd Zd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZd^ediej                  fdZdefdZej<                  ZdaePdz  eTd<   dae"dz  eTd<   defdZdefdZdefdZd ZddlZ ejJ                  ejN                  e        ejJ                  ejP                  e       d Zd Zedk(  r e         e        yy)zUTelegram bot for live monitoring and control of the sandman TV sleep pressure system.    N)RotatingFileHandler)Path)Update
BotCommandInlineKeyboardButtonInlineKeyboardMarkup
WebAppInfoMenuButtonWebAppKeyboardButtonReplyKeyboardMarkup)ApplicationCommandHandlerCallbackQueryHandlerContextTypes)	ParseModez&https://a0d7b954-ssh.tail821319.ts.nettimez/panel.html?v=z/remote.html?v=z/status.html?v=z.8782685448:AAG2QRMgxr1WGc1J8VcQGwxmNmj7rDDDnBIz/share/.ha_cache/.ha_wd.logz/share/.ha_cache/.ha_notify.logz /share/.ha_cache/.ha_notify_chatz/share/.ha_cache/.ha_wd_tv_ipz/share/sandman.pyz/share/.ha_cache/wd_config.jsonz/share/sandman_config_test.jsonz$/share/.ha_cache/.ha_wd_events.jsonlz!/share/.ha_cache/.ha_wd_action_ts
         z"/share/.ha_cache/.ha_wd_state.jsonz"/share/.ha_cache/.ha_wd_speed.jsong~jth?gMb@?z06:00z18:00      ?)startendratez22:00g333333?22:30g333333?z00:00g?z02:00g      @g      @returnc                      	 t        t              5 } t        j                  |       cd d d        S # 1 sw Y   y xY w# t        $ r dd d di dcY S w xY w)N        r   )deficitlast_updatedsession_starttotal_watch_today_min	daily_log)open
STATE_FILEjsonload	Exceptionfs    /share/.ha_cache/ha_notify.py
read_stater+   A   sL    z*99Q<  ztfgvxyyzs#   < 0	< 9< < AAstatec                 ~    t        t        d      5 }t        j                  | |d       d d d        y # 1 sw Y   y xY w)Nwr   )indent)r#   r$   r%   dump)r,   r)   s     r*   write_stater1   I   s*    	j#	!		%1% 
		s   3<hourminutec                 4   | dz  |z   }t         D ]  }t        t        |d   j                  d            \  }}t        t        |d   j                  d            \  }}|dz  |z   }|dz  |z   }	|	|k  r|	dz  }	||k\  r|n|dz   }
||
cxk  r|	k  sn |d   c S  y)z4Return the time-of-day multiplier for a given HH:MM.<   r   :r     r   r   )TIME_MULTIPLIERSmapintsplit)r2   r3   tentryshsmehemset_adjs              r*   _time_multiplier_atrE   N   s    r	FA!S%...s34BS%,,,S12BGbLGbL6LA!VW>>=  "     minutesc                 `    t        |       dz  }t        |       dz  }|dkD  r	| d|ddS | dS )zFormat minutes as Xh Ym.r5   r   zh 02dm)r:   )rG   hrJ   s      r*   _fmt_durationrL   _   sE    GAGrA1uBqgQS7NrF   r   targetc                     | |k  ry|dk  rd}t        j                  || z         t        z  }|dz  }|dk  rdt        |       dS d|dd	S )
zNEstimate hours to recover from deficit to target (rough, assumes no watching).nowr   gMbP?r5      ~rJ   .0frK   )mathlogRECOVERY_RATEr:   )r   rM   rG   hourss       r*   _estimate_recoveryrW   h   sh    &{xx())M9GbLEqy3w<.""uSkrF   sandman_botFz)%(asctime)s  %(levelname)-8s  %(message)sz%Y-%m-%d %H:%M:%S)datefmti    )maxBytesbackupCountc                      	 t        t              5 } t        | j                         j	                               }ddd       t        j
                         z
  t        k  S # 1 sw Y   'xY w# t        $ r Y yw xY w)z?Check if sandman executed an action within the suppress window.NF)r#   ACTION_MARKERfloatreadstripr   ACTION_SUPPRESS_Sr'   )r)   tss     r*   _sandman_action_recentrd      s`    - Aqvvx~~'(B !		b $555 !   s'   A+ (A%A+ A($A+ +	A76A7action
event_typec                 d   ddl }|j                   j                         }|j                  d      |j                  d      | |d}|t        |d      |d<   	 t	        t
        d      5 }|j                  t        j                  |      d	z          ddd       y# 1 sw Y   yxY w# t        $ r Y yw xY w)
z:Append a structured event to the shared events JSONL file.r   Nz%Y-%m-%dz%H:%M)dater   re   typerZ   r   a
)
datetimerO   strftimeroundr#   EVENTS_FILEwriter%   dumpsr'   )re   rf   r   _dtrO   r=   r)   s          r*   	log_eventrs      s    
,,


C\\*-s||G7Lz3E !,i+s#qGGDJJu%,- $## s0   B# &(BB# B B#  B# #	B/.B/owner_chat_idtv_ip
last_powerlast_applast_playback
last_title
log_offsetlast_volumeforce_rediscoverc                    t         r| st         S | sct        j                  j                  t              r@t        t              j                         j                         }|r|a |t        _	        t         S t        j                         a t         r1t        t              j                  t                t         t        _	        t         S )z.Return cached TV IP, re-discovering if needed.)ru   ospathexistsTV_IP_CACHEr   	read_textra   
tv_controlTV_IPdiscover_tv
write_text)r|   cacheds     r*   	get_tv_ipr      s     %{ ;k",,.446E%JL""$E[$$U+ 
LrF   c                     t         rt         S t        j                  j                  t              r@t        t              j                         j                         } | r| a | t        _	        t         S da t         t        _	        t         S )zHReturn TV IP, preferring cache even if TV is off (for standby commands).z192.168.1.50)
ru   r~   r   r   r   r   r   ra   r   r   )r   s    r*   get_tv_ip_or_cachedr      s_     	ww~~k"k",,.446E%JLEJLrF   c                  Z    d a 	 t        j                  t               y # t        $ r Y y w xY wN)ru   r~   remover   FileNotFoundError rF   r*   invalidate_tv_ipr      s*    E
		+ s    	**r   timeoutc                 >   t               }|sy t        j                  | ||      }|st        dz  at        dk\  r_t        j                         }|rG||k7  rB|a|t        _        t        t              j                  |       t        j                  d||       da|S da|S )Nr   iprP   r   zTV IP changed: %s -> %sr   )r   r   js_get_consecutive_failuresr   ru   r   r   r   r   rT   info)r   r   r   resultnew_ips        r*   r   r      s    		BtW<F~" B&++-F&B,#)
 [!,,V42B?$%! M !"MrF   datac                 N    t               }|syt        j                  | |||      S )NFr   )r   r   js_post)r   r   r   r   s       r*   r   r      s'    		BdD'bAArF   keyc                 D    t               }t        j                  | |      S Nr   )r   r   js_key)r   r   s     r*   send_keyr      s    		BSR((rF   c                  B    t               } t        j                  |       S r   )r   r   
get_volumer   s    r*   upnp_get_volumer      s    		B  B''rF   volc                 F    t               }t        j                  | |       y r   )r   r   
set_volume)r   r   s     r*   upnp_set_volumer     s    		B#"%rF   mutedc                 F    t               }t        j                  | |       y r   )r   r   set_mute)r   r   s     r*   upnp_set_muter   
  s    		B"%rF   c                      t         j                  j                  t              r5	 t	        t        t              j                         j                               S y # t        $ r Y y w xY wr   )	r~   r   r   CHAT_ID_FILEr:   r   r   ra   r'   r   rF   r*   
load_ownerr     sU    	ww~~l#	tL)335;;=>>   		s   3A 	A&%A&chat_idc                     | a t        t              j                  t	        |              t
        j                  d|        y )NzOwner chat_id saved: %d)rt   r   r   r   strrT   r   )r   s    r*   
save_ownerr     s-    M!!#g,/HH&0rF   updatec                     t         
t               a t          t        | j                  j                         y| j                  j                  t         k(  S )NT)rt   r   r   effective_chatid)r   s    r*   is_ownerr   "  sC    "6((++,  ##}44rF   c                     	 t        j                  ddgd      } t        j                         }| j	                         D ]8  }d|v sd|vsd|vs|j                         }t        |d         }||k7  s6|c S  	 y # t        $ r Y y w xY w)	NpsauxT)textz
sandman.pyrX   greprP   )
subprocesscheck_outputr~   getpid
splitlinesr;   r:   r'   )outmy_pidlinepartspids        r*   find_sandman_pidr   /  s    
%%tUm$?NN$Dt#T(AfTXFX

%(m&=J %   s0   AA< A< A< #A< 5A< 9A< <	BBc                      t               } | r:	 t        j                  dt        |       gd       t        j                  d|        yy# t        $ r }t        j                  d|       Y d }~yd }~ww xY w)NkillT)checkzKilled sandman PID %dzFailed to kill sandman: %sF)r   r   runr   rT   r   r'   error)r   rC   s     r*   kill_sandmanr   >  sg    

C
	7NNFCH-T:HH,c2   	7II2A66	7s   8A 	A1A,,A1configc                    	 t        j                  dt        d| ddgdt         j                  t         j                        }t        j                  d|j                  |        y# t        $ r }t        j                  d|       Y d }~y	d }~ww xY w)
Npython3z--configz	--log-dirz/share/Tstart_new_sessionstdoutstderrz%Started sandman PID %d with config %szFailed to start sandman: %sF)	r   PopenSANDMAN_SCRIPTDEVNULLrT   r   r   r'   r   )r   procrC   s      r*   start_sandmanr   J  sx    
FKS"%%%%	
 	8$((FK 		/3s   A A# #	B,BBc                  x    	 t        j                  ddgdd       y# t        t         j                  f$ r Y yw xY w)NadbversionT)capture_outputr   F)r   r   r   CalledProcessErrorr   rF   r*   adb_availabler   \  s<    y)$dKz<<= s    99c                  B    t               } t        j                  |       S r   )r   r   adb_get_current_appr   s    r*   r   r   d  s    		B))R00rF   c                  X    t               sy t               } t        j                  |       S r   )r   r   r   adb_screenshotr   s    r*   take_screenshotr   i  s#    ?		B$$++rF   r   c                     t        j                  d|       }|rG|j                  d      |j                  d      |j                  d      }}}|dk(  rd| d| S | d| S | j                         S )zFormat a sandman log line for Telegram readability.
    Input:  '2026-03-20 23:09:11 [INFO]    Grace period: 54 min ...'
    Output: '23:09 Grace period: 54 min ...'
    z6\d{4}-\d{2}-\d{2} (\d{2}:\d{2}):\d{2} \[(\w+)\]\s*(.*)rP   r   rZ   WARNINGu   ⚠️  )rematchgroupra   )r   rJ   r<   levelmsgs        r*   fmt_log_liner   r  st     	JDQA
AGGAJ
#5IQCq&&AcU|::<rF   ctxc                    K   t        |       sy | j                  j                  dt        j                         d {    y 7 w)Nuq   🌙 *Sandman Bot*

Use the command menu (tap /) for all controls.
Log updates will be pushed here automatically.
parse_mode)r   message
reply_textr   MARKDOWN)r   r   s     r*   	cmd_startr     sB     F
..
#
#	9 %%	 $   s   ;AAAc           
        K   t        |       sy | j                  j                  d       d {   }t               }g }|s|j	                  d       nt        d      }|r|j                  dd      nd}|dk(  rdn|dk(  rd	nd
}|j	                  | d|        |j	                  d|        t        d      }|rS|j                  dd      }	|j                  dd      }
|j                  d      rdnd}|j	                  | d|	 d|
        n!t               }||j	                  d| d       t               \  }}}|rt        j                  ||      }|rd| dnd}|j	                  d| |        |r|j                  d      r5d|d    }|j                  d      r|d|d    z  }|j	                  |       |j                  d      rt        |d   d       \  }}|j	                  d!| d"|d#       n^t        d$      }|rQ|j                  d%i       j                  d&d      }|r-|d'k7  r(|j	                  dt        j                  ||              t               }|r	 t        j                  d(d)gd*d*d+,      }d-|j                  v rd.nd/}d0d l}d}d}t!        t"        d1      5 }|j%                         d2d  D ]r  }d3|v r+|j'                  d4|      }|sd5|j)                  d6       d7}2d8|v s7|j'                  d9|      }|sLd:|j)                  d+       d;|j)                  d6       d}t 	 d d d        d0d l} | j*                  j-                         }!|d.k(  rt.        nt0        }"t!        |"      5 }t3        j4                  |      }#d d d        #j                  d<d=      }$|#j                  d>d?      }%|#j                  d@d       }&t7        t8        |$j;                  d"            \  }'}(|!j=                  |'|(d0A      })|!j>                  dBk  r$|'dBkD  r|)j=                  |)j@                  d6z
  C      })|!|)k\  s|!j>                  dDk  r>t9        |!|)z
  jC                         d z        dEz  }*|j	                  dF|$ dG|% dH|* dI       n:t9        |)|!z
  jC                         d z        }+|j	                  dF|$ dG|% d|+ dJ       |j	                  dK| dL| d       |r|j	                  |       |r|j	                  |       n|j	                  dN       t        j                  g dOd*d*P      },dQ|,j                  v r|j	                  dR       tF        jH                  jK                  dS      }-|-r|j	                  dT       tM               }.|.j                  dUdV      }/d0}0|.j                  dW      rp	  j*                  jO                  |.dW         }1| j*                  j-                  |1jP                  xs | jR                  jT                        |1z
  jC                         d z  }0|.j                  dXd0      }2|j	                  dY|/dZd[tW        |0       d\tW        |2              |jY                  d]j[                  |             d {    y 7 T# 1 sw Y   RxY w# 1 sw Y   xY w# tD        $ r |j	                  dM| d       Y w xY w# tD        $ r Y w xY w7 Vw)^Nu   ⏳ Checking...u&   📺 TV IP: unknown (never discovered)
powerstate?unreachableOn   🟢Standbyu   🔴   ❓z Power: u	   🌐 IP: audio/volumecurrentmaxr   u   🔇u   🔊z	 Volume: /   🔊 Volume: z (UPnP) () u
   📱 App: title   🎬 artist    — 
position_sr5   u   ⏱ r6   rI   zactivities/current	componentpackageNameNAr   r   TrZ   r   r   r   config_testtestprodr   rizGrace periodu$   Grace period — (\d+) min remainingu   ⏳ Grace: rP   z min remainingzsev=z#sev=([\d.]+) max_sev=(\d+) \| (\S+)u   ⚡ Last action: z (sev bad_hours_startr   bad_hours_peakz00:30grace_period_minutes)r2   r3   second   )day   r7   u   🌙 Bad hours:    →u	    (IN — zm in)zm until)u   🤖 Sandman: z mode (PID u   🤖 Sandman: running (PID u   🤖 Sandman: stoppedtcqdiscshowdevend0r   r   htbu   🌐 Throttle: ON#/share/.ha_cache/.netmon_state.jsonu   🔀 MITM: activer   r   r    r!      🧠 Deficit: .2fz | Session: 
 | Today: rk   ).r   r   r   r   appendr   getr   r   	APP_NAMESdivmodr   r   r   r   r   r#   LOG_FILE	readlinessearchr   rl   rO   TEST_CONFIGPROD_CONFIGr%   r&   r9   r:   r;   replacer2   r  total_secondsr'   r~   r   r   r+   fromisoformattzinfotimezoneutcrL   	edit_textjoin)3r   r   r   r   linespwpowericonvol_datacurmxr   uvpkgplaybackextrasfriendlystatus
title_lineminssecsactpr   r  config_name_re
grace_infoseverity_infor)   r   rJ   rl   rO   config_pathcfg	bad_startbad_peak	grace_minbhbmbad_timemins_in
mins_untiltc_checkmitm_runningr,   dsession_minss	today_mins3                                                      r*   
cmd_statusr_    s|    F))*;<
<C		BE=>L!-/|S)]$vey6HFevXeW-.y%&.),,y#.CeS)B&ll73FELLE7)C5"67 "B~}RD89 3 5Xv }}S#.H)1r(1%rFLL:hZx89::g&#((9!:Jzz(+"fX.>-?&@@
LL,::l+!'|(<b!AJD$LL4vQtCj!9: -.CGGK,00CdLL:immAq.A-B!CD 
C
1	?e}TVWXA$1QXX$=&6K JMh$KKM#$/D%-JJ'NPTU+6qwwqzl.)QJ4JJ'MtT.?
|6RSRYRYZ[R\Q]]^,_M 0 % ##'')C)4)>+KKk"aiil # 17;Iww/9H 6;Iiooc23FB{{2a{@Hxx"}b#++q0@+Ah#((Q,sX~<<>CDP/	{#hZyQXPYY^_` (S.!?!?!AB!FG
/	{#hZr*U]^_LL>+k#aHIZ(]+ 	,- ~~DUY`deH()77>>"GHL() LE		)S!AKyy!	""001GHB#,,001Sh>O>O>S>STWYYhhjmooK 		115I	LL>!C];5O4PPZ[hir[sZtuv
--		%(
)))w =t %$  #".  	?LL6se1=>	?*  		
 *s   +[ZH6[%AZ */ZZ5Z
)Z4AZ 6ZE1Z =B,[*A/[ A"[;[<[Z	Z ZZ [ <[?[  [	[[[[c                 f  K   t        |       sy t               s$| j                  j                  d       d {    y | j                  j                  d       d {   }t	        j
                  t               d {   }|rt        j                  j                  |      rit        j                  j                  |      dkD  rG| j                  j                  t        |d             d {    |j                          d {    y |j                  d       d {    y 7 7 7 7 =7 '7 w)Nu   ❌ ADB not installedu   📸 Taking screenshot...r   rb)photou/   ❌ Screenshot failed — TV off or DRM content)r   r   r   r   asyncio	to_threadr   r~   r   r   getsizereply_photor#   deleter7  )r   r   r   r   s       r*   cmd_screenshotrh    s     F?nn''(?@@@))*EF
FC""?33Dt$)>)Bnn((tD$/?(@@@jjlmmMNNN 	A
F3@Nsj   5D1D%#D1D' D1<D)=A0D1-D+.D1D-D1D/ D1'D1)D1+D1-D1/D1c                 t  K   t        |       sy t        j                  d        d {   }|r\|j                  dd       t        j                  fd       d {    | j                  j                  rdnd        d {    y | j                  j                  d       d {    y 7 7 S7 -7 w)Nc                      t        d      S Nr   r   r   rF   r*   <lambda>zcmd_mute.<locals>.<lambda>"  s	    vn/ErF   r   Fc                      t               S r   )r   )new_mutes   r*   rm  zcmd_mute.<locals>.<lambda>%  s
    h(?rF   
   🔇 Muted   🔊 Unmutedu   ❌ Can't reach TV)r   rc  rd  r)  r   r   )r   r   r=  ro  s      @r*   cmd_muterr    s     F&&'EFFH||GU33 ?@@@nn''8<*XZZZnn''(<=== G 	AZ=sE   &B8B04B8B2'B8B4#B8*B6+B82B84B86B8c           
      h  K   t        |       sy |j                  }|r|d   j                         st        j                  d        d {   }|rJ| j
                  j                  d|j                  dd       d|j                  dd              d {    y | j
                  j                  d       d {    y t        d	t        dt        |d                     t        j                  fd
       d {    | j
                  j                  d        d {    y 7 7 7 r7 17 w)Nr   c                      t        d      S rk  rl  r   rF   r*   rm  zcmd_volume.<locals>.<lambda>0  s	    6.3IrF   r  r   r   r  r  zUsage: /volume N (0-60)r5   c                      t               S r   )r   )r   s   r*   rm  zcmd_volume.<locals>.<lambda>7  s
    OC$8rF   u   🔊 Volume → )r   argsisdigitrc  rd  r   r   r)  minr  r:   )r   r   rv  r=  r   s       @r*   
cmd_volumery  +  s    F88DtAw( **+IJJ..++mHLLTW<X;YYZ[c[g[ghmor[sZt,uvvv 	 ..++,EFFF
b#aT!W&
'C


8
999
..
#
#&6se$<
=== KvF :=s\   AD2
D(A
D2D*#D29D,:AD2<D.=%D2"D0#D2*D2,D2.D20D2c                    K   t        |       sy t        j                  d        d {   }| j                  j	                  |rdnd       d {    y 7 ,7 w)Nc                      t        d      S )NPauser   r   rF   r*   rm  zcmd_pause.<locals>.<lambda>>  s	    '):rF   u
   ⏸ Paused
   ❌ Failedr   rc  rd  r   r   r   r   oks      r*   	cmd_pauser  ;  sJ     F  !:;	;B
..
#
#BLL
III 
<I!   &AA&AAAAc                    K   t        |       sy t        j                  d        d {   }| j                  j	                  |rdnd       d {    y 7 ,7 w)Nc                      t        d      S )NPlayr}  r   rF   r*   rm  zcmd_play.<locals>.<lambda>E  	    &)9rF   u   ▶️ Playingr~  r  r  s      r*   cmd_playr  B  sK     F  !9:	:B
..
#
#$4
MMM 
;Mr  c                    K   t        |       sy t        j                  d        d {   }| j                  j	                  |rdnd       d {    y 7 ,7 w)Nc                      t        d      S )NHomer}  r   rF   r*   rm  zcmd_home.<locals>.<lambda>L  r  rF   u	   🏠 Homer~  r  r  s      r*   cmd_homer  I  sJ     F  !9:	:B
..
#
#2K<
HHH 
;Hr  c                   K   t        |       sy t               }|s$| j                  j                  d       d {    y t	        j
                  d        d {   }| j                  j                  |rdnd       d {    y 7 K7 .7 	w)NuD   ❌ TV IP unknown — turn TV on manually first so I can discover itc                      t        d      S )Nr   r}  r   rF   r*   rm  zcmd_power.<locals>.<lambda>W  s	    ))<rF   u   ⚡ Power toggledu0   ❌ Failed — TV may be fully off (not standby))r   r   r   r   rc  rd  )r   r   r   r  s       r*   	cmd_powerr  P  sx     F		Bnn''(nooo  !<=	=B
..
#
#2$7Cu
vvv 	p	=vs3   7BBBB&B?B	 BB	Bc                    K   t        |       syt               }d|d<   t        j                  j                         j	                         |d<   t        |       | j                  j                  d       d{    y7 w)z;Quick test: set deficit to 0.7 for immediate heavy actions.Nffffff?r   r   uH   ⚡ Test mode — deficit set to 0.70
Heavy actions should trigger soon.)r   r+   rl   rO   	isoformatr1   r   r   )r   r   r,   s      r*   cmd_testr  [  se     FLEE)$--113==?E.
..
#
#$o
ppps   A4A>6A<7A>c                   K   t        |       sy | j                  j                  d       d {   }t                t	        j
                  d       d {    t        t              }|j                  |rdnd       d {    y 7 X7 37 	w)Nu   ⏳ Switching to production...rP   u   🌙 Production mode ON   ❌ Failed to start)	r   r   r   r   rc  sleepr   r0  r7  )r   r   r   r  s       r*   cmd_prodr  f  sp     F))*JK
KCN
--
	{	#B
--R1=R
SSS	 LSs3   +BB&BB+B B
BB
Bc                    K   t        |       sy t               }| j                  j                  |rdnd       d {    y 7 w)Nu   🛑 Sandman stoppedu   ℹ️ Sandman wasn't running)r   r   r   r   r  s      r*   cmd_stopr  p  s6     F	B
..
#
#b$:Fe
fffs   9AAAc                    K   t        |       sy t               r$| j                  j                  d       d {    y t	        t
              }| j                  j                  |rdnd       d {    y 7 <7 w)Nu   ℹ️ Sandman already runningu   ✅ Sandman startedr  )r   r   r   r   r   r0  r  s      r*   cmd_start_sandmanr  w  sh     Fnn''(HIII	{	#B
..
#
#R$9EZ
[[[ 	J \s!   5A8A46A8.A6/A86A8c                 Z  K   t        |       sy t        j                  j                  t              s$| j
                  j                  d       d {    y 	 t        t        d      5 }|j                         }d d d        d}|j                  r?|j                  d   j                         r"t        dt        |j                  d               }t              |k\  r|| d  n|}|D cg c]  }|j                         st        |        }}dj!                  |      }|sd}t        |      dkD  r|d	d  }| j
                  j                  |       d {    y 7 # 1 sw Y   xY wc c}w 7 # t"        $ r1}	| j
                  j                  d
|	        d {  7   Y d }	~	y d }	~	ww xY ww)Nu   ℹ️ No log filer     r   2   rk   z(empty)  i`u   ❌ Error: )r   r~   r   r   r,  r   r   r#   r-  rv  rw  rx  r:   lenra   r   r8  r'   )
r   r   r)   r9  ntaill	formattedr   rC   s
             r*   cmd_logr    s[    F77>>(#nn''(<===;(C AKKME !88++-BCHHQK()A Z1_uaRSz%.2@daggi\!_d	@yy#Dt9t<Dnn''--- 	> !  A 	. ;nn''+aS(9:::;s   AF+EF+E. 'E8A/E. 'E'=E'
AE. E,E. F+E$ E. .	F(7!F#FF#F+#F((F+c                 X  K   t        |       sy|j                  }t        j                  j	                  d      }|s,| j
                  j                  d|rdnd d       d{    y|d   j                         }|dk(  r|r$| j
                  j                  d	       d{    y| j
                  j                  d
       d{   }t        j                  ddgdt        j                  t        j                         t        j                  d       d{    t        j                  j	                  d      r|j                  d       d{    y|j                  d       d{    y|dk(  r|s$| j
                  j                  d       d{    yt        j                          t!                t        j"                  g dd       t        j                  d       d{    | j
                  j                  d       d{    yy7 7 w7 V7 7 7 7 7 ;7 w)u/   Control ARP MITM daemon — /mitm on|off|statusNr$  u   🔀 MITM: ONOFFuQ   

/mitm on — start (needed for throttle)
/mitm off — stop and restore networkr   onu   ℹ️ MITM already runningu   ⏳ Starting MITM...r   /share/.ha_cache/netmon.pyTr      u,   🔀 MITM ON — TV traffic flows through Piu   ❌ MITM failed to startoffu   ℹ️ MITM not running)pkillz-fzarp_mitm.pyr   u"   🔀 MITM OFF — network restored)r   rv  r~   r   r   r   r   lowerr   r   r   rc  r  r7  r   throttle_remove_clear_throttle_stater   )r   r   rv  runningargr   s         r*   cmd_mitmr    s    F88DggnnBCGnn'''$u5 65 56 	6 	6 	
q'--/C
d{..++,IJJJNN--.DEE)%ABVZ * 2 2:;M;M	OmmA77>>?@-- NOOO-- :;;;	..++,EFFF""$5dKmmAnn''(LMMM 
'	6 KE 	O; G 	Ms   A H*"H#=H* H!#H*HAH*H7H*H H**H"+*H*H$AH**H&+"H*H(H*H*H*H* H*"H*$H*&H*(H*z/share/device_names.json)switch.sonoff_10020b052f_1switch.sonoff_10020b052f_2switch.sonoff_10020b0c7c_1switch.sonoff_10020b0c7c_2%switch.magic_switch_s1e_bb55_switch_1%switch.magic_switch_s1e_bb55_switch_2%switch.magic_switch_s1e_bb55_switch_3-switch.magic_switch_s1e_595a_kitchen_switch_1-switch.magic_switch_s1e_595a_kitchen_switch_2-switch.magic_switch_s1e_595a_kitchen_switch_3zswitch.tz3000_cayepv1a_ts011fzswitch.tz3000_cayepv1a_ts011f_2switch.tz3000_cayepv1a_ts011f_3z	light.fanzlight.fan_2zlight.fan_3c                      	 t        t              5 } t        j                  |       cd d d        S # 1 sw Y   y xY w# t        $ r i cY S w xY wr   )r#   DEVICE_NAMES_FILEr%   r&   r'   r(   s    r*   _get_device_namesr    s<    #$99Q< %$$ 	s#   < 0	< 9< < A
	A
methodc                 0    t        j                  | ||      S r   )r   ha_api)r  r   r   s      r*   _ha_apir    s    VT400rF   c                      t               } t        j                  t              }g }|D ],  \  }}}| j	                  ||      }|j                  |||f       . |S )z0Returns [(entity_id, friendly_name, state), ...])r  r   ha_get_device_states
HA_DEVICESr)  r(  )namesrawr   eiddefault_namer,   names          r*   _get_ha_device_statesr    sZ    E

)
)*
5CF$' \5yyl+sD%() %( MrF   r  )r  r  r  r  r  )r  r  r  r  r  )u   🛏 Bedroomu   🛋 Living Roomu   🍽 Diningu   🍳 Kitchenu   🧺 Laundryc                    K   t        |       syt        dt        t                    gg}| j                  j                  dt        |             d{    y7 w)zOpen devices Mini App.Nu   🏠 Open Devicesurlweb_appTap to open:reply_markup)r   r   r	   
WEBAPP_URLr   r   r   r   r   keyboards      r*   cmd_devicesr    s^     F%z*  H ..
#
#)(3 $      AAAAc                    K   t        |       syt        dt        t                    gg}| j                  j                  dt        |             d{    y7 w)zOpen TV remote Mini App.Nu   📺 Open TV Remoter  r  r  r  )r   r   r	   TV_REMOTE_URLr   r   r   r  s      r*   cmd_tvr    s^     F%}-  H ..
#
#)(3 $   r  c                    K   t        |       syt        dt        t                    gg}| j                  j                  dt        |             d{    y7 w)z$Open Sandman observability Mini App.Nu   🌙 Open Sandmanr  r  r  r  )r   r   r	   SANDMAN_APP_URLr   r   r   r  s      r*   cmd_sandman_appr  #  s^     F%/  H ..
#
#)(3 $   r  c                 6  K   | j                   }|r|j                  j                  d      sy|j                          d{    |j                  dd j	                  d      d   t        j                  fd       d{   }|s|j                  d       d{    y|j                  dd	      }|d
k(  rdndt        j                  fd       d{    t        j                  d       d{    t        j                  t               d{   }g }|D ];  \  }}}	|	d
k(  rdn|	d	k(  rdnd}
|j                  t        |
 d| d|       g       = |j                  dt        |             d{    y7 F7 7 7 7 7 w7 w)z"Handle device toggle button press.zdev:N   .r   c                  "    t        dd        S )NGETz/states/r  )r  s   r*   rm  z!device_callback.<locals>.<lambda><  s    (3%@P1QrF   u   ❌ Device unreachabler,   r  r  turn_offturn_onc                  .    t        dd  d di      S )NPOSTz
/services/r  	entity_idr  )domainr  services   r*   rm  z!device_callback.<locals>.<lambda>C  s"    GFj'4SVacfUg$hrF   g      ?r   u   ⚫r   r   )callback_datau   🏠 Devices — tap to toggler  )callback_queryr   
startswithanswerr;   rc  rd  edit_message_textr)  r  r  r(  r   r   )r   r   query
state_datar   devicesr  d_eidr  r,   r<  r  r  r  s              @@@r*   device_callbackr  1  s    !!E

--f5
,,.
**QR.CYYs^AF (()QRRJ%%&>???nnWe,G#tOjG


h
iii --
%%&;<<GH%tU$vUe^E-fAdV .
  	 & 
!
!()(3 "   5  S?
 j <s   >FF	AFFF!F"=FF F<F= FFA%FFFFFFFFFc                 :  K   t        |       sy| j                  j                  d       d{   }t                t	        j
                  d        d{   }|r|j                  d|        d{    y|j                  d       d{    y7 e7 ?7 #7 w)zForce re-discover TV IP.Nu   🔍 Scanning...c                      t        d      S )NT)r|   )r   r   rF   r*   rm  zcmd_discover.<locals>.<lambda>\  s	    D)IrF   u   📺 Found TV at u   ❌ TV not found)r   r   r   r   rc  rd  r7  )r   r   r   r   s       r*   cmd_discoverr  V  s     F))*<=
=C  !IJ	JB	mm/t4555mm./// >	J5/sE   +BB'BBB3B4BBBBBBc                   K   t        |       syt               | j                  j                  d       d{   }fd}t	        j
                  |       d{   }|j                  |       d{    | j                  j                  t        rdj                  t              nd       d{    y7 ~7 ^7 G7 w)z3Measure actual TV bandwidth + show throttle status.Nu   ⏳ Measuring speed...c            	         g } 	 t        j                  g dddd      }d|j                  v rt        j                  g dddd      }dd l}|j	                  dd	|j                  v r|j                  j                  d	      d
   nd      }|r|j                  d      nd}| j                  d|        n| j                  d       s"| j                  d       dj                  |       S 	 t        j                  dddddgddd      }dd l}|j	                  d|j                        }|rht        |j                  d            }|dk  r| j                  d|dd       n2|dk  r| j                  d|dd       n| j                  d|dd        |j	                  d!|j                        }|rAt        |j                  d            dkD  r$| j                  d"|j                  d       d#       	 dd l}|j                         }	d}
d%D ]N  }	 t        j                  |d&      }|
|r,t        t!        j"                  |      j%                               ndz  }
P |j                         |	z
  }|dkD  r1|
dkD  r,|
d'z  |z  d(z  }| j                  d)|dd*|
d+z  dd,|d-d.       | j                  d       | j                  d/       | j                  d0       dj                  |       S # t        $ r | j                  d       Y Aw xY w# t        $ r | j                  d$       Y 5w xY w# t        $ r Y ,w xY w# t        $ r Y w xY w)1N)r  -sr  r  r   r!  Tr  r  r#  )r  classr  r   r!  r   zrate (\d+[KMG]?bit)z1:20r  rP   r   u    🔴 Throttle ACTIVE — limit: u    🟢 Throttle OFF — full speedu   ❓ Throttle status unknownu   📶 TV IP unknownrk   pingz-c3z-W2r   z([\d.]+)/([\d.]+)/([\d.]+)r   u   📶 Ping: rR   zms (excellent)   z	ms (good)z	ms (slow)z(\d+)% packet lossu   ⚠️ Packet loss: %u   📶 Ping failed)systemapplicationszchanneldb/tv/channelLists/allr      i  u   📊 API throughput: z kbps (i   zKB in z.1fs)u   📺 Netflix requirements:z*  4K = 25mbps, 1080p = 5mbps, 720p = 3mbps)r   r   r   r   r.  r;   r   r(  r'   r8  r_   r:   r   r   r   r  r%   rq   encode)r9  r  rcrL  rJ   r   avgloss_m_timer   total_bytesendpointelapsedkbpsr   s                 r*   measurezcmd_netspeed.<locals>.measurej  s+   	8K.2qJA ^^$J37dAO JJ5V\`b`i`iViryyv7Nr7Roqr%&qwwqzC?vFG?@ LL-.99U##	-c4bA.2rKA

8!((CAAGGAJ'7LL;s3i~!FG2XLL;s3iy!ABLL;s3iy!ABZZ 5qxx@F#fll1o.23FLLO3DAFG
	 JJLEKW"))(A"EA!3tzz!}';';'=#>JK X jjlU*G{{Q#a72T94T#JgkRVFVWZE[[abijmannpqr
 	R12AByyg  	8LL67	80  	-LL+,	- !   		s]   CK* +DL 4L< AL,AL< *LLL)(L),	L95L< 8L99L< <	MMrk   zNo data)	r   r   r   r   rc  rd  r7  r9  r8  )r   r   r   r  r   r   s        @r*   cmd_netspeedr  c  s     F		B))*BC
CCD L $$W--F
--

..
#
#DIIe$49
MMMW DP .MsE   5B?B7!B?B9B?3B;4=B?1B=2B?9B?;B?=B?z/share/sandman_throttle_activec                      t         ^ddl} | j                  j                  dt              }| j                  j                  |      a |j                  j                  t                t         S )zImport sandman.py as a module.Nr   sandman)_sandman_modimportlib.utilutilspec_from_file_locationr   module_from_specloaderexec_module)	importlibspecs     r*   _load_sandman_moduler!    sO     ~~55iP ~~66t<-rF   c                     t         j                  j                  t              syt        j                  d       t        j                          	 t        j                  t        t              j                               } | j                  d      }|rt               }|j                  |      }|j                         |_        |j#                  |      |_        |j#                  d      |_        |j$                  r2|j&                  r&|j)                          t        j                  d|       	 t/        j0                  g dd	       	 t        j2                  t               t        j                  d
       y# t*        $ r }t        j-                  d|       Y d}~nd}~ww xY w# t*        $ r Y gw xY w# t*        $ r Y \w xY w)zAOn startup, clean up any orphaned tc rules from a previous crash.NuA   Found orphaned throttle state — cleaning up tc rules and ARP...ru   z192.168.1.254zARP restored for %sz%Orphan cleanup ARP restore failed: %s)sysctlz-wznet.ipv4.ip_forward=0Tr  zOrphaned throttle cleaned up)r~   r   r   THROTTLE_STATE_FILErT   r   r   r  r%   loadsr   r   r)  r!  LinuxBandwidthThrottler_get_our_mac_our_mac_resolve_mac_tv_mac_gw_mac_restore_arpr'   warningr   r   r   )r,   ru   modr<   rC   s        r*   _cleanup_orphaned_throttler/    sQ   77>>-.HHPQ @

4 34>>@A		'"&(C++E2A)AJu-AI7AIyyQYY .6@QUV
		%& HH+,  @;Q??@    s=   CE6 -F" F1 6	F?FF"	F.-F.1	F=<F=bwc                 l    t        t              j                  t        j                  | |d             y )Nru   r0  )r   r$  r   r%   rq   r2  s     r*   _save_throttle_stater3    s$    	((e24N)OPrF   c                  V    	 t        j                  t               y # t        $ r Y y w xY wr   )r~   r   r$  r   r   rF   r*   r  r    s&    
		%& s    	((c                      t         rt         j                  rt         S t               } t               }|sy| j	                  |      a t         S )z<Import and return a LinuxBandwidthThrottler from sandman.py.N)
_throttleractiver!  r   r&  )r.  r   s     r*   _get_throttlerr8    sA     j''

 C		B,,R0JrF   c                   K   t        |       sy |j                  }|srt        j                  g ddd      }d|j                  v }|r$| j
                  j                  d       d {    y | j
                  j                  d       d {    y |d   j                         }|dk(  rrt        r*t        j                         st        j                          d at        j                          t                | j
                  j                  d	       d {    y |d
k(  rdn@|j                         rt        |      n$| j
                  j                  d       d {    y dt!        |      dk\  r!|d   j                         rt        |d         t#               s$| j
                  j                  d       d {    y t$        j&                  j)                  d      }|s| j
                  j                  d d       d {   }t        j*                  ddgdt        j,                  t        j,                         t/        j0                  d       d {    t$        j&                  j)                  d      sA|j3                  d       d {    y | j
                  j                  d d       d {   }	 t/        j4                  fd       d {   }t7               dkD  rn|j3                  d d d       d {    fd}	t        r(t        j                         st        j                          t/        j8                   |	             ay |j3                  d d       d {    y 7 7 7 p7 +7 7 7 67 7 7 7 7 ## t:        $ r'}
|j3                  d|
        d {  7   Y d }
~
y d }
~
ww xY ww) Nr  Tr"  r#  u9   🌐 Throttle is ON

/throttle off — restore full speedu  🌐 Throttle is OFF

/throttle 2000 — degraded (deficit 0.50+)
/throttle 1000 — visibly bad quality
/throttle 800 — very degraded (deficit 0.70+)
/throttle 500 — barely loads
/throttle 2000 60 — for 60s

Sandman auto-throttles based on deficit level.r   r  u)   🌐 Throttle OFF — full speed restoredr  i@  u   Usage:
/throttle 5000 — throttle to 5mbps (auto-off 120s)
/throttle 5000 60 — throttle for 60s
/throttle off — restore full speedr   rP   u   ❌ TV IP unknownr$  u   ⏳ Starting MITM + zkbps throttle...r   r  r   r  u*   ❌ MITM failed to start — TV may be offu   ⏳ Applying c                  2    t        j                         S )N)ru   bandwidth_kbps)r   throttle_apply)r0  r   s   r*   rm  zcmd_throttle.<locals>.<lambda>:  s    Z-F-FR`b-crF   u   🌐 Throttle ON — zkbps (auto-off in r
  c                    K   t        j                          d {    t        j                          t	                t
        r3	 t        j                  j                  t
        d  d       d {    y y 7 \7 # t        $ r Y y w xY ww)Nu   🌐 Throttle auto-off after u   s — full speed restoredr   r   )
rc  r  r   r  r  rt   appbotsend_messager'   )durations   r*   auto_offzcmd_throttle.<locals>.auto_off@  s     mmH---**,%' !gg22=Qnownx  yR  PS2  T  T  T ! .
 T$ sE   BA9(B,A= 2A;3A= 7B;A= =	B	BB		Bz kbps (use /throttle off to stop)u   ❌ Throttle error: )r   rv  r   r   r   r   r   r  _throttle_timerdonecancelr   r  r  rw  r:   r  r   r~   r   r   r   r   rc  r  r7  rd  r3  create_taskr'   )r   r   rv  rY  is_throttledr  rZ  r   r  rC  rC   r0  rB  r   s              @@@r*   cmd_throttlerI    sZ    F88D>>"HY]dhi/..++,ijjj 	 ..++  -|  }  }  }
q'--/C
e|?#7#7#9""$"O""$nn''(STTT d{	Xnn''  )u  v  	v  	vH
4yA~$q'//+tAw<		Bnn''(;<<< 77>>"GHLNN--0DRDHX.YZZ)%ABVZ * 2 2:;M;M	OmmAww~~CD-- LMMMNN--bTAQ.RSS8$$%cddR$a<--"7t;MhZWY Z[[[ ';';'=&&(%11(*=O--"7t;[ \]]]K k } 	U 	v 	= [ 	M Td \  ^ 8mm21#67778s   A#O(N )#ONB	ONAON	A O?N AON	AON7ON'O<N=ON  N0N  NAN  !O"N  :N;N  ?OOO	OOOOOON  N  N   	O)O OOOOOc                 4  K   t        |       sy|j                  }|r	 t        |d         }t        dt	        d|            }t               }||d<   t        j                  j                         j                         |d<   t        |       | j                  j                  d|d       d{    yt               }|j                  dd      }d}|j                  d
      r|	 t        j                  j                  |d
         }t        j                  j                  |j                  xs t        j                   j"                        |z
  j%                         dz  }|j                  dd      }t)        |d      }	t)        |d      }
d|ddt+        |       dt+        |       d|	 d|
 d}| j                  j                  |       d{    y7 $# t        $ r' | j                  j                  d	       d{  7   Y yw xY w# t&        $ r Y w xY w7 Jw)z*Show or set the current attention deficit.Nr   r   r   r   r   u   ✅ Deficit set to r&  zUsage: /deficit [0.0-1.0]r    r5   r!   g333333?g?r%  u   
📊 Session: r'  u   
🔮 Recovery: z to 0.15 | z to 0.05)r   rv  r_   r  rx  r+   rl   rO   r  r1   r   r   
ValueErrorr)  r3  r4  r5  r6  r2  r'   rW   rL   )r   r   rv  valr,   r[  r\  r]  r^  recovery_15recovery_05r   s               r*   cmd_deficitrO  W  s     F88D 	Q.Cc3sC=)CLE"E)$,$5$5$9$9$;$E$E$GE.!..++.A#c,KLLL LE		)S!AKyy!	""001GHB#,,001Sh>O>O>S>STWYYhhjmooK 		115I$Q-K$Q-K 3  &{34J}Y?W>X Y%k+h	H 	
 ..
#
#D
)))5 M 	..++,GHHH	  		 *sy   HBG -G.G 20H#A;H A-HHHG 'H;G><HHHH	HHHHc                 Z  K   t        |       sy|j                  }|rt        |      dk  r$| j                  j	                  d       d{    y|d   j                         }	 d|v rt        |j                  dd            }n.d|v rt        |j                  dd            dz  }nt        |      }|dz  }	 t        t        |d
   j                  d            \  }}t               }|j                  dd      }	|	}
||}}t        t        |            D ]^  }t        ||      }|d
z   }t         t#        j$                  d
|dz  z         z  |z  }|	|z  }	t'        |	d      }	|d
z  }|dk\  sUd}|d
z   dz  }` |dd|d}d|d    d|d
    d|
dd|	dd| d|	|
z
  d}| j                  j	                  |       d{    y7 w# t        $ r' | j                  j	                  d	       d{  7   Y yw xY w# t        $ r' | j                  j	                  d       d{  7   Y yw xY w7 nw)zXSimulate deficit after watching N hours starting at HH:MM.
    Usage: /simulate 3h 23:00Nr   zUsage: /simulate 3h 23:00r   rK   r  rJ   r5   z Bad duration. Use e.g. 3h or 90mrP   r6   zBad time. Use e.g. 23:00r   r   r   r      rI   u   📈 Simulation: z from z
Start deficit: r&  z
End deficit:   z (at z)
Delta:         +)r   rv  r  r   r   r  r_   r1  rK  r9   r:   r;   r+   r)  rangerE   BASE_IMPACT_RATErS   rT   rx  )r   r   rv  dur_strrV   	total_minstart_hstart_mr,   r   start_deficitcur_hcur_m_tmsession_min_so_farimpactend_timer   s                      r*   cmd_simulater`    s`     F88D3t9q=nn''(CDDD 1gmmoG
'>'//#r23EG^'//#r23b8E'NEBJ	sDGMM#$67
 LEii	3'GM G5E3y>" .U!DHHQ1Cb1H-H$IIBN6gs#
B;EQY"$E # AeC[)H
DG9F47) 4', -!#eH: 6"]237	9 	 ..
#
#D
)))a 	E  nn''(JKKK  nn''(BCCC8 *s   AH+	G 
H+#AG 7%G6 B
H+'AH+:H);H+'G3*G-+G30H+2G33H+6'H&H H&#H+%H&&H+c           	        K   t        |       syt               }|j                  dd      }t        j                  j	                         }|j
                  |j                  }}g }d}d}	|j                  d      rS	 t        j                  j                  |d         }
t        d||
j                  d      z
  j                         dz        }	t        d	      D ]  }|||d
z  z   dz  z   dz  }||d
z  z   dz  }|dd|d}|}t        |d
z        D ]^  }|	|z   dz   }t        |||z   dz  z   dz  ||z   dz        }t        t        j                   d|d
z  z         z  |z  }||z  }t#        |d      }` t%        ||z        }d|z  d||z
  z  z   }|dk\  rdnd}|j'                  | d| d|d|         | j(                  j+                  ddj-                  |      z   dz   t.        j0                         d{    y# t        $ r Y <w xY w7 w)z<Show text bar chart of deficit projection over next 4 hours.Nr   r   r  r   r    )r4  r5   	   r   rQ  rI   r6   rP   r   u   █u   ░r  u    ⚡r  r   r&  u+   📊 Deficit projection (if watching):
```
rk   z
```r   )r   r+   r)  rl   rO   r2   r3   r3  r  r1  r2  r'   rR  rE   rS  rS   rT   rx  r:   r(  r   r   r8  r   r   )r   r   r,   r   rO   rY  rZ  r9  	BAR_WIDTHr]  r]  stept_ht_mlabelsim_deficitrJ   r?   r\  r^  filledbarwarns                          r*   	cmd_curverl    s]    FLEii	3'G





!C88SZZ5E EIyy!	""001GHB!$Qrzzz/F)F(U(U(WZ\(\!]
 ar	)b00B6tby B&s)1SI& tby!A#a'!+B$euqyR.?&?2%EPQ	UWGWXB%R"W(==BF6!Kk3/K " [9,-fnu	F(:;;$+vwauAk#%6tf=># & ..
#
#$SVZV_V_`eVf$fip$p  ~G  ~P  ~P
#  Q  Q  Q/  		. Qs8   A6H9AG; D*H5H6H;	HHHHc                 Z  K   t        |       sy|j                  }|sh	 t        t              5 }t	        j
                  |      }ddd       j                  dd      }| j                  j                  d| d       d{    y	 t        |d         }|dk  s|dkD  r$| j                  j                  d	       d{    y	 t        t        d      5 }t	        j                  d|i|       ddd       |dk(  r$| j                  j                  d       d{    y| j                  j                  d| d       d{    y# 1 sw Y   xY w# t        $ r d}Y w xY w7 7 # t        $ r' | j                  j                  d
       d{  7   Y yw xY w# 1 sw Y   xY w7 7 iw)zHSet sandman speed multiplier. /speed 10 = 10x faster, /speed 1 = normal.NspeedrP   u   ⏩ Speed multiplier: zx
Usage: /speed 10 or /speed 1r   g?d   u   Speed must be 0.1–100zUsage: /speed 10r.   u   ⏩ Speed reset to 1x (normal)u   ⏩ Speed set to x)r   rv  r#   
SPEED_FILEr%   r&   r)  r'   r   r   r_   rK  r0   )r   r   rv  r)   r   mults         r*   	cmd_speedrs    s    F88D	j!Qyy| "88GQ'D nn''*@Fe(fgggT!W~#:..++,EFFF $ 
j#	!		7D/1% 
 qynn''(HIIInn''*;D6(CDDD/ "!  	D	g G nn''(:;;; 
	 	JDs   F+E EE "F+?E$ F+6E( <E&=E( F+F,+F+F''F+?F) F+E
E E!F+ E!!F+&E( ('FFFF+FF+F$ F+)F+c           	      $  K   t        |       syt               }|j                  di       }|s$| j                  j	                  d       d{    ydg}t        |j                               dd }|D ]d  }||   }t        |t              r|j                  dd      }|dk(  r|j                  d| d	       E|j                  d| d
t        |              f | j                  j	                  dj                  |             d{    y7 7 w)z1Show daily watch history from sandman_state.json.Nr"   u   📅 No watch history yet.u   📅 Watch history:i	watch_minr   z  u   : 0m (rest day ✅)z: rk   )r   r+   r)  r   r   sortedkeys
isinstancedictr(  rL   r8  )r   r   r,   r"   r9  sorted_datesdate_strrG   s           r*   cmd_historyr|    s     FLE		+r*Inn''(DEEE"#E)..*+CD1L H%gt$kk+q1Ga<LL2hZ':;<LL2hZr-*@)ABC ! ..
#
#DIIe$4
555 	F 6s%   A	DDB:DDDDr?  c                   K   t         j                  j                  t              r$t         j                  j	                  t              anda	 t        j                  t               d{    t        s)	 t         j                  j                  t              sNt         j                  j	                  t              }|t
        k  rda|t
        k(  rt        t        d      5 }|j                  t
               |j                         }|j                         addd       sg }|D ]  }|j                         }|s	 t        j                   |      }|j%                  dd      }|dk(  rD|j%                  dd      }|j%                  dd      }	|j%                  d	      }
|d
k(  r%|
d|
ddnd}|j'                  d| d|	 |        |dk(  s|j'                  d| d|	         |sdj)                  |      }t+        dt-        |      d      D ]4  }|||dz    }	 | j.                  j1                  t        |       d{    6 	 7 # 1 sw Y   ;xY w# t"        $ r Y Cw xY w7 )# t"        $ r }t2        j5                  d|       Y d}~d}~ww xY w# t"        $ r }t2        j5                  d|       Y d}~td}~ww xY ww)z?Poll sandman_events.jsonl for new entries and push to Telegram.r   Nr  ri   r  userr   re   r   r  z [r&  ]u   🤖 r   r  u   🌟 rk   r  r>  zFailed to send log: %szLog watcher error: %s)r~   r   r   ro   re  rz   rc  r  LOG_POLL_INTERVALrt   r#   seekr-  tellra   r%   r%  r'   r)  r(  r8  rR  r  r@  rA  rT   r-  )r?  sizer)   	new_linesr  r  evetyper<   re   r   d_strr   ichunkrC   s                   r*   log_watcherr  '  st     
ww~~k"WW__[1


mm-....	477>>+.77??;/Dj 
z!k3'1z"KKM	VVX
 (  IGGIAB vr*F?FF62&"-&&+I%3:3FbQ/BE$$uQCq%@Ah&$$uQCq%9:% & 99Y'D1c$i.Qq4x(=''..}5.QQQ /W . (' ! & R  =KK 8!<<= 	4KK/33	4s   A'K)I*K6#J K7J KJ "6I
J "K#J >IA<J J +K-2J  $I0I.I0	J KIJ 	I+'J *I++J .I00	J9JJ JJ 	K%K ;K KK
last_mutedlast_ambilightc                 
  K   	 t        j                  d       d{    t        s%	 t        j                  d        d{   }|r|j	                  d      }t
        `|t
        k7  rWt               rdnd}|dk(  rdnd	}	 | j                  j                  t        | d
| d|        d{    t        d|        |a|dk(  rt        j                  t               d{   \  }}}|r|t        k7  rt        j	                  ||      }t        t               rdnd}| d| g}	|r|	dxx   d| dz  cc<   |r(|j	                  d      r|	j                  d|d           	 | j                  j                  t        dj                  |	             d{    t        d|        |a|ra|t         k7  rXt         Pt               rdnd}|dk(  rdn|dk(  rdnd}	 | j                  j                  t        | d
| d
|        d{    |a|rr|j	                  d      ra|d   }
|
t"        k7  rSt"        Kd|
 }|j	                  d      r|d|d    z  }	 | j                  j                  t        |       d{    |
at        j                  d        d{   }|r|j	                  d       }|j	                  d!d"      }|t$        |t$        k7  r|t$        z
  }t               rdnd}|dkD  rd#nd$}	 | j                  j                  t        | d
| d%t$         d&| d|dkD  rd'nd( | d       d{    t        d)t$         d*| d|dkD  rd'nd( | d       |at&        Y|t&        k7  rPt               rdnd}	 | j                  j                  t        | d
|rd+nd,        d{    t        |rd-nd.       |at        j                  d/        d{   }|r|j	                  d0d(      }t(        F|t(        k7  r=t               rdnd}	 | j                  j                  t        | d1|        d{    |an5t
        dk(  r,da	 | j                  j                  t        d2       d{    |7 d7 A7 # t        $ r Y w xY w7 7  # t        $ r Y 
w xY w7 # t        $ r Y w xY w7 A# t        $ r Y Kw xY w7 47 # t        $ r Y w xY w7 9# t        $ r Y Cw xY w7 7 # t        $ r Y w xY w7 # t        $ r Y w xY w# t        $ r }t*        j-                  d3|       Y d}~d}~ww xY ww)4uZ   Periodically check TV state and notify on changes — power, app, volume, mute, ambilight.r  Nc                      t        d      S )Nr   rl  r   rF   r*   rm  zstate_monitor.<locals>.<lambda>s  s	    1ErF   r   u   🤖u   👤r   u   📺u   💤r   u    TV → r>  u   TV → u    📱 App → r   r  r  video_idu   🎬 youtu.be/rk   u   App → PLAYINGu   ▶️PAUSEDu   ⏸u   ⏹r  r  r	  r
  c                      t        d      S rk  rl  r   rF   r*   rm  zstate_monitor.<locals>.<lambda>  s	    vn?UrF   r   r   Fu   🔺u   🔻z Volume u    → +r  zVolume r  rp  rq  MutedUnmutedc                      t        d      S )Nzambilight/powerrl  r   rF   r*   rm  zstate_monitor.<locals>.<lambda>  s
    6BS;TrF   r;  u    💡 Ambilight → u   ❓ TV unreachablezState monitor error: %s)rc  r  rt   rd  r)  rv   rd   r@  rA  r'   rs   r   rw   r*  r(  r8  rx   ry   r{   r  r  rT   r-  )r?  r:  r;  whor<  rA  rB  rC  rD  r   r  msg_textr=  r>  r   diffarrowambi
ambi_staterC   s                       r*   state_monitorr  k  s     mmA}	6(()EFFB|,)ez.A$:$<&&C%*d]6D!gg22$13%qhug8V 3   
 w/0"
D=292C2CDW2X,X)C6sh#,==c#:#/,B,D&&C(+uN8*%E$FE' %ab
!,< <%&**Z*@ %~fZ>P=Q-R S%&)gg&:&:,9		%@P '; '" !" !"
 &
&;<#&  H$=(4,B,D&&C/79/D8S[_gSg%mrD%&)gg&:&:,93%qaPXz@Z '; '" !" !"
 )1 &**W"5 &w J.)5-25'?#)::h#7$,%x8H7I0J$JH!)*-''*>*>0=H +? +& %& %&
 */J &-%6%67U%VVH&ll95 (We < ?{/F3R]K]#&#4D,B,D&&C.2QhFFE%&)gg&:&:,9,/5%USVRWWYaehiaiZ]oqYrswrxxy)z '; '" !" !" &}CuBdUVhs\^F_`d_eef&gh&) &1ez6I,B,D&&C%&)gg&:&:,9,/5%,^1\)] '; '" !" !" &gIF%*
 ")!2!23T!UUD%)XXgr%:
)5*:V,B,D&&C%&)gg&:&:,9,/50DZL)Q '; '" !" !" *4 %!%J!gg22$18L 3   w  G %  -Y!" $- % $%!" $- % $%%& (1 !)$(!)
  W!" $- % $%!" $- % $% V!" $- % $% %  	6KK1155	6s  UR UT# R>T# ,R	 .R/R	 32T# %R&BT# )3R RR !?T# !,R2 R/R2 A
T# $S SS T# !S"AT# >A S >S?S AT# -S. 2S+3S. 7*T# !S>"5T# )T TT T# $T 9T:T >UT# R	 		RT# RT# R 	R,(T# +R,,T# /R2 2	R?;T# >R??T# S 	ST# ST# S 	S($T# 'S((T# +S. .	S;7T# :S;;T# T 	TT# TT# T 	T T# T  T# #	U,UUUUc           
      6
  K   t               st        j                  d       yi ddddddd	d
dddddddddddddddddddddd d!d"d#d$i d%d&d'd(d)d*d+d,d-d.d/d0d1d2d3d4d5d6d7d8d9d:d;d<d=d>d?d@dAdBdCdDdEdFdGdHdIdJ}d}dK}	 t        st	        j
                  dM       d{    %|st               }|st	        j
                  dN       d{    Q| dO	 t	        j                  fdP       d{   }dQ|j                  j                         v sdR|j                  j                         v rdL}t        j                  dS       nMt        j                  dT|j                  j                                t	        j
                  dU       d{    	 | dOd}	 t	        j                  dWdXdYdZd[t        j                  j                  t        j                  j                  \       d{   }	 	 t	        j                   |j                  j#                         d]^       d{   }|sn|j)                  d`a      j                         }db|vsdc|vre|j+                         }	d}
|	D ]w  }|j-                  dd      r|j/                  ddde      }
 nRt1        |      dfk(  s7t3        dg |j                         D              sX|j5                  |j                         |      }
y |
r-	 | j6                  j9                  t        dh|
 i       d{    #|r5|j&                  )	 |j;                          |j=                          d{    dK}t        j                  dk       t	        j
                  dN       d{    7 7 7 7 # t        $ r?}t        j                  dV|       t	        j
                  dU       d{  7   Y d}~Sd}~ww xY w7 7 # t        j$                  $ r3 |j&                  #t        j                  d_|j&                         Y Y 3w xY w7 # t        $ r Y $w xY w# t        $ r!}t        j                  dj|       Y d}~Kd}~ww xY w7 !# t        $ r Y +w xY w7 # |rF|j&                  :	 |j;                          |j=                          d{  7   n# t        $ r Y nw xY wdK}t        j                  dk       t	        j
                  dN       d{  7   w xY ww)lzLMonitor TV remote key presses via ADB getevent. Requires wireless debugging.u,   ADB not available — skipping input monitorN0073zVol+0072zVol-0074Power0066r  009eBack0160z
OK/Confirm0067Up006cDown0069Left006aRight00a4r|  00cfr  00d0	PlayPause00a5FastFwd00a6Rewind0071Mute0192Menu0174Exit0193Red0194Green0195Yellow0196Blue00e2zCH+00e3zCH-0090Num00087Num10088Num20089Num3008aNum4008bNum5008cNum6008dNum7008eNum8008fNum9InfoGuideSource)0166016b00aeFTr  r   z:5555c                  <    t        j                  dd gddd      S )Nr   connectTr   r  )r   r   )rM   s   r*   rm  z#adb_input_monitor.<locals>.<lambda>  s     JNNE9f+E:>TSUWrF   	connectedalreadyz(ADB connected to %s for input monitoringzADB connect failed: %sr   zADB connect error: %sr   r  shellgeteventz-l)r   r   r5   )r   z%ADB getevent process exited (code %s)r1  )errorsEV_KEYDOWNKEY_r  r  c              3   $   K   | ]  }|d v  
 yw)0123456789abcdefNr   ).0cs     r*   	<genexpr>z$adb_input_monitor.<locals>.<genexpr>F  s     *VIq10B+BIs   u   👤 🎮 Remote: r>  zADB getevent error: %su8   ADB input monitor disconnected — will reconnect in 10s)r   rT   r   rt   rc  r  r   rd  r   r  ra   r'   r-  create_subprocess_execr   PIPEwait_forreadlineTimeoutError
returncodedecoder;   r  r1  r  allr)  r@  rA  r   wait)r?  	KEY_NAMESr   r  r  rC   r   r   r   r   	key_labelrJ  rM   s               @r*   adb_input_monitorr    s    ??@(.9?.4d<BF 	  *0 ;A& 		 $Y	 17	
 	
 
 )/ 	 w )/ ;A& 	 u '-f 7=f 	  )/ 9? 	  )/ 9? I 
BI
--""" $&BmmB'''t5\F!++WX X !((.."22i188>>CS6S $IHHGPHH5qxx~~7GH!--+++	 Q 4u6	$ 77tVWj$))..)).. D !(!1!1$++2F2F2HRT!UUD {{){4::<4'6+=

 	A||F+$%IIfb$9	1v{s*VAGGI*V'V$-MM!'')Q$?	  !gg22$1:LYK8X 3   ; N /IIK))+%% IHHOP--###k " (X , 3Q7mmB''' V++ 2!H$//Z4 %   	5KK0!44	5 &   $ /IIK))+%%  IHHOP--###s  B4T7N8+T#N$
T/N( 
N"BN( &N%'N( +
T6AQ O3Q 2O9 ?O6 O9 A6Q ; Q $Q 'Q (Q)Q -Q /T>#R !R"R &/TRTT"N( %N( (	O01.O+O" O+%T+O00T3Q 6O9 9?P?8Q >P??Q Q 	QQ QQ 	Q?Q:4R :Q??R R 	RTRTT'#S
SST	STS2TTTTc                 j  K   t               at        j                  dt               | j                  j                  t        dd      t        dd      t        dd      t        dd	      t        d
d      t        dd      t        dd      t        dd      g       d{    t        r	 t        t        dt        t        dz               t        dt        t                    t        d	t        t        dz               ggdd      }| j                  j                  t        d|       d{    t!        j"                  t$               d{    t        j                  d       t!        j"                  t&               d{   }t        j                  d|       t!        j(                  t+        |              t!        j(                  t-        |              t!        j(                  t/        |              y7 a7 # t        $ r }t        j                  d|       Y d}~d}~ww xY w7 7 w)z&Start background tasks after bot init.zBot starting. Owner chat_id: %srE  zQuick overviewtvu   📺 TV Remoter  u   🌙 Sandmanr  u   🏠 Devicesr;  zToggle TV powerpausez
Play/PausemutezToggle muter]  zScreenshot to chatNz?v=3r  r  T)resize_keyboardis_persistentu   ⚙️ Bot restarted)r   r   r  zFailed to set keyboard: %szDiscovering TV...z	TV IP: %s)r   rt   rT   r   r@  set_my_commandsr   r   r   r	   r  r  r  rA  r'   r-  rc  rd  r/  r   rG  r  r  r  )r?  r  rC   r   s       r*   	post_initr  `  s     LMHH.>
''
!
!8-.4)*9n-9n-7-.7L)6=)4-.	# 	 	 	 	9*"#3ZM\bLb=cd">:/;Z[">:*W]J];^_ 
 !%"H ''&&}CYhp&qqq
 

6
777HH !  +	+BHH["C()c*+)#./I	. r 	9KK4a88	9 8 
,sn   BH3G>
H3)BH 1H2H 6H3H/5H3H1	A6H3H 	H,H'"H3'H,,H31H3c                 F   t         j                  d|        t        rDt        j                  r4	 t        j	                          t                t         j                  d       t        j                  d       y# t        $ r }t         j                  d|       Y d}~:d}~ww xY w)z0Clean up throttle on SIGTERM/SIGINT before exit.u%   Received signal %d — cleaning up...zThrottle stopped gracefullyzThrottle cleanup failed: %sNr   )
rT   r   r6  r7  stopr  r'   r-  sysexit)signumframerC   s      r*   _graceful_shutdownr
    so    HH4f=j''	:OO!#HH23 HHQK  	:KK5q99	:s   3A7 7	B  BB c                     t        j                         j                  t              j	                  t              j                         } | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        dt                     | j                  t        d	t                     | j                  t        d
t                      | j                  t        dt"                     | j                  t        dt$                     | j                  t        dt&                     | j                  t        dt(                     | j                  t        dt*                     | j                  t        dt,                     | j                  t        dt.                     | j                  t        dt0                     | j                  t        dt2                     | j                  t        dt4                     | j                  t        dt6                     | j                  t        dt8                     | j                  t        dt:                     | j                  t        dt<                     | j                  t?        t@        d             | j                  t        dtB                     | j                  t        dtD                     | j                  t        dtF                     tH        jK                  d       | jM                  tN        jP                         y ) Nr   rE  r   simulatecurvern  history
screenshotr]  r  volumer  playhomer;  r  r  r  r   rT   discoverr  r  r  z^dev:)patternmitmnetspeedthrottlezSandman bot starting polling...)allowed_updates))r   buildertoken	BOT_TOKENr  buildadd_handlerr   r   r_  rO  r`  rl  rs  r|  rh  rr  ry  r  r  r  r  r  r  r  r  r  r  r  r  r  r   r  r  r  rI  rT   r   run_pollingr   	ALL_TYPES)r?  s    r*   mainr     sf   




%
%i
0
:
:9
E
K
K
MCOON7I67OON8Z89OON9k:;OON:|<=OON7I67OON7I67OON9k:;OON<@AOON489OON6845OON8Z89OON7I67OON6845OON6845OON7I67OON6845OON6845OON6845OON?4EFGOON5'23OON:|<=OON9k:;OON401OON9o>?OO('JKOON6845OON:|<=OON:|<=HH./OOF$4$4O5rF   c                     d} t        j                         }	 t         j                  j                  |       rat	        t        |       j                         j                               }||k7  r,t        j                  |d       t        j                  d|       t        | d      5 }|j                  t        |             ddd       y# t        t        f$ r Y Aw xY w# 1 sw Y   yxY w)zFKill any other sandman_bot processes before starting. Uses a PID file.z/share/.ha_cache/.ha_notify.pidrb  zKilled stale bot process %dr.   N)r~   r   r   r   r:   r#   r`   ra   r   rT   r   ProcessLookupErrorrK  rp   r   )pidfiler   old_pidr)   s       r*   _ensure_single_instancer%    s    /GYY[F77>>'"$w-,,.4467G& #6@ 
gs	q	F 
	 
+ 		s   B C $CCCC&__main__)r~  N)F)r  r   )__doc__rc  rl   r%   loggingrS   r~   r   r   r  r   logging.handlersr   pathlibr   tvmodr   telegramr   r   r   r   r	   r
   r   r   telegram.extr   r   r   r   telegram.constantsr   	_BASE_URLr   r:   
__import___Vr  r  r  r  r,  BOT_LOG_FILEr   r   r   r0  r/  ro   r^   rb   STATE_POLL_INTERVALr  r$   rq  rS  rU   r8   ry  r+   r1   r_   rE   rL   rW   	getLoggerrT   handlersclearsetLevelINFO	propagate	Formatter_fmt_rhsetFormatter
addHandlerStreamHandler_shboolrd   rs   rt   __annotations__ru   rv   rw   rx   ry   rz   r{   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   tupler   r   r   DEFAULT_TYPEr   r_  rh  rr  ry  r  r  r  r  r  r  r  r  r  r  r  r  r  r  listr  DEVICE_AREASr  r  r  r  r  r  r6  rD  r  r$  r!  r/  r3  r  r8  rI  rO  r`  rl  rs  r|  r  r*  r  r  r  r  r  r
  signal_signalSIGTERMSIGINTr   r%  __name__r   rF   r*   <module>rL     s}   [      	 	  
  0   W  W  W X X (4	Z$$&	'({.-
+_RD1Krd3 =	(01-$// 53    2
1
  gs3gs3gs3gs3gs3gs3 zD z&t &
c 3 5 "5 S  u   g&     W\\ wDNab,J     s g     s  c s e " !sTz  sTz 
C$J #*  sTz  
C$J 
C S4Z 
 t $S4Z     u TD[ ,B# BT BE B$ B)# )$ )(t (
& &
& &C$J 1 15V 5 5#* 	d 	# $ $t 1U 1
,t ,s s $F )B)B ~*V ~*,*C*C ~*BO Ol.G.G O	>6 	>(A(A 	>>V >,*C*C > JF J)B)B JN6 N(A(A NI6 I(A(A IwF w)B)B wq6 q(A(A qT6 T(A(A Tg6 g(A(A g\F \1J1J \;& ;|'@'@ ;0%N6 %N(A(A %NP / 
(4 1C 1s 1$ 1$+ 1	t 	 	% 	%$ 	8'2f <+D+D  l&?&? & |/H/H "& "|/H/H "J
0v 
0L,E,E 
0PNv PNL,E,E PNf 
6 -@Q Q Q
R8v R8L,E,E R8p)*f )*<+D+D )*X7*v 7*L,E,E 7*t)QF )Q)B)B )QXEF E)B)B ED6f 6<+D+D 68:4; :4z   	 
D4K !d
 !D6[ D6Nk$ k$\*0 *0`
  w 2 3 w~~1 2!6H  zF rF   