
    `gjS                     `   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mZ ddlm	Z	m
Z
mZmZ 	 ddlZdZddlmZmZ ddlmZmZmZmZ  ej0                  e      Zd	efd
Zded	efdZ G d de      Zdddddededee   dee    ded	e
ee	f   fdZ!d	efdZ"d Z#ddZ$y# e$ r dZdZY xw xY w)a  
Home Assistant platform adapter.

Connects to the HA WebSocket API for real-time event monitoring.
State-change events are converted to MessageEvent objects and forwarded
to the agent for processing.  Outbound messages are delivered as HA
persistent notifications.

Requires:
- aiohttp (already in messaging extras)
- HASS_TOKEN env var (Long-Lived Access Token)
- HASS_URL env var (default: http://homeassistant.local:8123)
    N)datetime)AnyDictOptionalSetTF)PlatformPlatformConfig)BasePlatformAdapterMessageEventMessageType
SendResultreturnc                      t         S )z;Check if Home Assistant runtime dependencies are available.)AIOHTTP_AVAILABLE     Z/root/.hermes/venv/lib/python3.12/site-packages/plugins/platforms/homeassistant/adapter.pycheck_ha_requirementsr   *   s    r   configc                 ~    t        | dd      xs t        j                  dd      j                         }t	        |      S )zHReturn True when Home Assistant has enough credential config to connect.tokenN
HASS_TOKEN )getattrosgetenvstripbool)r   r   s     r   validate_ha_configr   /   s3    VWd+Jryyr/JQQSE;r   c                   <    e Zd ZdZdZg dZdef fdZdefdZ	dd	d
e
de
fdZde
fdZddZddZddZddZdeeef   ddfdZededeeef   deeef   dee   fd       Z	 	 d dededee   deeeef      def
dZd!deddfdZdedeeef   fdZ xZS )"HomeAssistantAdapterz
    Home Assistant WebSocket adapter.

    Subscribes to ``state_changed`` events and forwards them as
    MessageEvent objects.  Supports domain/entity filtering and
    per-entity cooldowns to avoid event floods.
    i   )   
      <   r   c                    t         |   |t        j                         d | _        d | _        d | _        d | _        d| _        |j                  xs i }|j                  xs t        j                  dd      }|j                  d      xs t        j                  dd      }|j                  d      | _        || _        t#        |j                  dg             | _        t#        |j                  d	g             | _        t#        |j                  d
g             | _        t+        |j                  dd            | _        t/        |j                  dd            | _        i | _        y )Nr   r   r   urlHASS_URLzhttp://homeassistant.local:8123/watch_domainswatch_entitiesignore_entities	watch_allFcooldown_secondsr$   )super__init__r   HOMEASSISTANT_session_ws_rest_session_listen_task_msg_idextrar   r   r   getrstrip	_hass_url_hass_tokenset_watch_domains_watch_entities_ignore_entitiesr   
_watch_allint_cooldown_seconds_last_event_time)selfr   r7   r   r'   	__class__s        r   r0   zHomeAssistantAdapter.__init__C   s%   !7!78 <@@D@D48 ";		, ;iiZ"))J8Y"Z!jjo % ),EIIor,J(K),UYY7G-L)M*-eii8I2.N*O $UYY{E%B C&)%))4F*K&L 35r   r   c                 D    | xj                   dz  c_         | j                   S )z%Return the next WebSocket message ID.   )r6   rD   s    r   _next_idzHomeAssistantAdapter._next_id^   s    ||r   F)is_reconnectrJ   c                  K   t         s!t        j                  d| j                         y| j                  s!t        j                  d| j                         y	 | j                          d{   }|syt        j                  t        j                  d            | _	        | j                  s8| j                  s,| j                  s t        j                  d| j                         t        j                  | j                               | _        d	| _        t        j%                  d
| j                  | j&                         y	7 # t(        $ r+}t        j+                  d| j                  |       Y d}~yd}~ww xY ww)z4Connect to HA WebSocket API and subscribe to events.z4[%s] aiohttp not installed. Run: pip install aiohttpFz[%s] No HASS_TOKEN configuredNr$   totaltimeoutz[%s] No watch_domains, watch_entities, or watch_all configured. All state_changed events will be dropped. Configure filters in your HA platform config to receive events.Tz[%s] Connected to %sz[%s] Failed to connect: %s)r   loggerwarningnamer;   _ws_connectaiohttpClientSessionClientTimeoutr4   r=   r>   r@   asynciocreate_task_listen_loopr5   _runninginfor:   	Exceptionerror)rD   rJ   successes       r   connectzHomeAssistantAdapter.connectg   s#     NNQSWS\S\]NN:DIIF	 ,,..G ")!6!6--B7"D
 &&t/C/CDOOA II	 !( 3 3D4E4E4G HD DMKK.		4>>J- /0  	LL5tyy!D	sO   AE:E +E,E 2E:3CE  E:E 	E7!E2-E:2E77E:c                   K   | j                   j                  dd      j                  dd      }| d}t        j                  t        j                  d            | _        | j
                  j                  |dd	       d
{   | _        | j                  j                          d
{   }|j                  d      dk7  r>t        j                  d|j                  d             | j                          d
{    y| j                  j                  d| j                  d       d
{    | j                  j                          d
{   }|j                  d      dk7  r/t        j                  d|       | j                          d
{    y| j                         }| j                  j                  |ddd       d
{    | j                  j                          d
{   }|j                  d      s/t        j                  d|       | j                          d
{    yy7 7 7 ;7 7 7 7 w7 W7 w)z0Establish WebSocket connection and authenticate.zhttps://zwss://zhttp://zws://z/api/websocketr$   rL   rN   )	heartbeatrO   Ntypeauth_requiredzExpected auth_required, got: %sFauth)rc   access_tokenauth_okzAuth failed: %ssubscribe_eventsstate_changed)idrc   
event_typer^   z!Failed to subscribe to events: %sT)r:   replacerT   rU   rV   r2   
ws_connectr3   receive_jsonr8   rP   r]   _cleanup_ws	send_jsonr;   rI   )rD   ws_urlmsgsub_ids       r   rS   z HomeAssistantAdapter._ws_connect   s    ''
H=EEiQXY8>*--))3
 11&BPR1SS HH))++776?o-LL:CGGFOL""$$$ hh   ,,"
  	 	 HH))++776?i'LL*C0""$$$ hh  &)"
  	 	 HH))++wwy!LL<cB""$$$K T , %	 , %
	 , %s   BIH1&I+H4,AI<H7=0I-H:.!IH=AIH?7I	I
!I+I,>I*I+I4I7I:I=I?IIIINc                 @  K   | j                   r8| j                   j                  s"| j                   j                          d{    d| _         | j                  r8| j                  j                  s"| j                  j                          d{    d| _        y7 W7 w)zClose WebSocket and session.N)r3   closedcloser2   rH   s    r   ro   z HomeAssistantAdapter._cleanup_ws   sm     88DHHOO((.."""==!5!5--%%'''	 # (s%   A BBA
BBBBc                   K   d| _         | j                  r6| j                  j                          	 | j                   d{    d| _        | j                          d{    | j                  r8| j                  j                  s"| j                  j                          d{    d| _        t        j                  d| j                         y7 # t        j                  $ r Y w xY w7 7 Iw)zDisconnect from Home Assistant.FNz[%s] Disconnected)rZ   r5   cancelrW   CancelledErrorro   r4   ru   rv   rP   r[   rR   rH   s    r   
disconnectzHomeAssistantAdapter.disconnect   s     $$&'''' !%D   d&8&8&?&?$$**,,,!'3 ())  	!,sY   .C/C  CC C/C+ AC/#C-$,C/C C(%C/'C((C/-C/c                 4  K   d}| j                   r	 | j                          d{    | j                   sy| j                  t        |t        | j                        dz
           }t
        j                  d| j                  |       t        j                  |       d{    |dz  }	 | j                          d{    | j                          d{   }|r"d}t
        j                  d| j                         | j                   ryy7 # t        j                  $ r Y yt        $ r,}t
        j                  d| j                  |       Y d}~)d}~ww xY w7 7 7 # t        $ r+}t
        j                  d| j                  |       Y d}~d}~ww xY ww)z,Main event loop with automatic reconnection.r   Nz[%s] WebSocket error: %srG   z[%s] Reconnecting in %ds...z[%s] Reconnectedz[%s] Reconnection failed: %s)rZ   _read_eventsrW   ry   r\   rP   rQ   rR   _BACKOFF_STEPSminlenr[   sleepro   rS   )rD   backoff_idxr_   delayr^   s        r   rY   z!HomeAssistantAdapter._listen_loop   sV    mmI''))) == ''KT=P=P9QTU9U(VWEKK5tyy%H--&&&1KM&&((( $ 0 0 22"#KKK 2DII>- mm)))  I9499aHHI ' )2  M=tyy!LLMs   FD DD A5FE 	F*E! =E>E! E(E! >FFD E"F$E,!EFEFE! E! !	F*!FFFFc                   K   | j                   | j                   j                  ry| j                   2 3 d{   }|j                  t        j                  j
                  k(  r^	 t        j                  |j                        }|j                  d      dk(  r)| j                  |j                  di              d{    |j                  t        j                  j                  t        j                  j                  hv s y7 7 J# t        j                  $ r' t        j                  d|j                  dd        Y w xY w6 yw)z.Read events from WebSocket until disconnected.Nrc   eventzInvalid JSON from HA WS: %s   )r3   ru   rc   rT   	WSMsgTypeTEXTjsonloadsdatar8   _handle_ha_eventJSONDecodeErrorrP   debugCLOSEDERROR)rD   ws_msgr   s      r   r|   z!HomeAssistantAdapter._read_events   s    88txx HH 		 		&{{g//444S::fkk2Dxx'72"33DHHWb4IJJJ !2!2!9!97;L;L;R;R SS		
 K++ SLL!>DS@QRS %s]   0EEDE(E#AD:D;D?AEEED6E=E EEr   c                 h  K   |j                  di       }|j                  dd      }|sy|| j                  v ryd|v r|j                  d      d   nd}| j                  s| j                  r=| j                  r|| j                  v nd}| j                  r|| j                  v nd}|s|sy| j
                  syt        j                         }| j                  j                  |d      }||z
  | j                  k  ry|| j                  |<   |j                  di       }	|j                  d	i       }
| j                  ||	|
      }|sy| j                  d
dddd      }t        |t        j                  |d| dt        |       t        j                                }| j#                  |       d{    y7 w)z2Process a state_changed event from Home Assistant.r   	entity_idr   N.r   F	old_state	new_state	ha_eventsHome Assistant EventschannelhomeassistantHome Assistant)chat_id	chat_name	chat_typeuser_id	user_nameha__)textmessage_typesource
message_id	timestamp)r8   r?   splitr=   r>   r@   timerC   rB   _format_state_changebuild_sourcer   r   r   rA   r   nowhandle_message)rD   r   
event_datar   domaindomain_matchentity_matchr   lastr   r   messager   	msg_events                 r   r   z%HomeAssistantAdapter._handle_ha_event  s    YYvr*
#R8	 --- -09,<%a("$"6"6<@<O<O6T%8%88UZL@D@T@T9(<(<<Z_L iik$$((A6$J$000+.i( NN;3	NN;3	++Iy)L ""-#& # 
 !$))YKqS
3lln
	 !!),,,s   F(F2*F0+F2r   r   r   c                    |sy|r|j                  dd      nd}|j                  dd      }||k(  ry|j                  di       j                  d|       }d| v r| j                  d      d   nd}|d	k(  rH|j                  di       }|j                  d
d      }|j                  dd      }	d| d| d| d| d|	 dS |dk(  r1|j                  di       j                  dd      }
d| d| |
 d| |
 S |dk(  rd| d|dk(  rdnd d|dk(  rd dS d dS |dv rd| d|dk(  rd S d S |d k(  rd| d!| d| d"S d| d#|  d$| d| d"	S )%z@Convert a state_changed event into a human-readable description.Nstateunknown
attributesfriendly_namer   r   r   climatecurrent_temperature?temperaturez[Home Assistant] z: HVAC mode changed from 'z' to 'z' (current: z
, target: )sensorunit_of_measurementz: changed from z to binary_sensor: on	triggeredclearedz (was >   fanlightswitchz	: turned offalarm_control_panelz: alarm state changed from ''z (z): changed from ')r8   r   )r   r   r   old_valnew_valr   r   attrstemptargetunits              r   r   z)HomeAssistantAdapter._format_state_changeB  s    7@)--3i--3 g!lB7;;OYW,/9,<%a(" YMM,3E992C8DYY}c2F#M? 39F7)<vZxqR
 X==r2667LbQD#M?/)D6gYtf6
 _$#M?"")T/;yA B'.$FaI =FFaI //#M?)"d?4680568
 **#M? 39F7)1.  b <$IVG9A7	
r   r   contentreply_tometadatac           	        K   | j                    d}d| j                   dd}d|d| j                   d}	 | j                  r| j                  j	                  |||t        j                  d	      
      4 d{   }|j                  dk  r=t        dt        j                         j                  dd       cddd      d{    S |j                          d{   }	t        dd|j                   d|	       cddd      d{    S t        j                         4 d{   }
|
j	                  |||t        j                  d	      
      4 d{   }|j                  dk  rNt        dt        j                         j                  dd       cddd      d{    cddd      d{    S |j                          d{   }	t        dd|j                   d|	       cddd      d{    cddd      d{    S 7 7 R7 <7 # 1 d{  7  sw Y   yxY w7 7 7 7 7 m7 B7 3# 1 d{  7  sw Y   nxY wddd      d{  7   y# 1 d{  7  sw Y   yxY w# t        j                  $ r t        dd      cY S t         $ r }t        dt#        |            cY d}~S d}~ww xY ww)zSend a notification via HA REST API (persistent_notification.create).

        Uses the REST API instead of WebSocket to avoid a race condition
        with the event listener loop that reads from the same WS connection.
        z,/api/services/persistent_notification/createBearer application/jsonAuthorizationzContent-TypezHermes AgentN)titler   r#   rL   )headersr   rO   i,  T   )r^   r   FzHTTP r   )r^   r]   z"Timeout sending notification to HA)r:   r;   MAX_MESSAGE_LENGTHr4   postrT   rV   statusr   uuiduuid4hexr   rU   rW   TimeoutErrorr\   str)rD   r   r   r   r   r'   r   payloadrespbodysessionr_   s               r   sendzHomeAssistantAdapter.send  s       LM&t'7'7&89.

 $7 7 78

	;!!--22# #11;	 3  
^ 
^
 {{S()$4::<CSCSTWUWCXY
^ 
^ 
^ &*YY[0)%t{{mSUVZU[?\]
^ 
^ 
^ #002 b bg&|| '$ ' 5 5B ?	  ,   
b 
b
 ;;,#-dtzz|GWGWX[Y[G\#]
b 
b 
bb b b *.#4D#-eU4;;-WYZ^Y_C`#a
b 
b 
bb b b
^ 
^  1
^ 
^ 
^ 
^b
b 
bb $5
bb
b 
b 
bb b b b b ## 	Ye3WXX 	;e3q6::	;s  4KAJ  :H;J  >;H 9J  HJ  
KH H H ?J  HJ  KJ  )H5*J  -.I+H8I+;II+&H:'I++J  7H<8J  <K=IH> I1I+=I >I+J  IJ  KJ  J  H J   H2&H)'H2.J  1K2J  8I+:I+<J  >I I+J  I	
II	I+J  $I'%J  *K+I=1I42I=9J  <K=J    K
 K"K
*K?K
 KK

Kc                    K   yw)z'No typing indicator for Home Assistant.Nr   )rD   r   r   s      r   send_typingz HomeAssistantAdapter.send_typing  s     s   c                 *   K   dd| j                   dS w)z-Return basic info about the HA event channel.r   r   )rR   rc   r'   )r:   )rD   r   s     r   get_chat_infoz"HomeAssistantAdapter.get_chat_info  s      ,>>
 	
s   r   N)NN)N)__name__
__module____qualname____doc__r   r}   r	   r0   rA   rI   r   r`   rS   ro   rz   rY   r|   r   r   r   r   staticmethodr   r   r   r   r   r   __classcell__)rE   s   @r   r!   r!   5   sP     %N5~ 56#  5: %T %d %N-4 -^4*M<8-DcN 8-t 8-t ;
;
S>;
 S>;
 
#	;
 ;
J #'-14;4; 4; 3-	4;
 4S>*4; 
4;l6 6 6
3 
4S> 
r   r!   )	thread_idmedia_filesforce_documentr   r   r   r   r   c                  K   t         sddiS t        | di       xs i }|j                  d      xs t        j                  dd      j                  d      }t        | dd	      xs t        j                  d
d      j                         }|r|sddiS | d}	d| dd}
||d}	 t        j                  t        j                  d            4 d	{   }|j                  |	|
|      4 d	{   }|j                  dvrN|j                          d	{   }dd|j                   d| icd	d	d	      d	{    cd	d	d	      d	{    S d	d	d	      d	{    d	d	d	      d	{    dd|dS 7 7 7 d7 B7 37 $# 1 d	{  7  sw Y   4xY w7 +# 1 d	{  7  sw Y   ;xY w# t        j                  $ r ddicY S t        $ r}dd| icY d	}~S d	}~ww xY ww)u  Send a notification via the HA ``notify.notify`` service without a
    live gateway adapter.

    Used by ``tools/send_message_tool._send_via_adapter`` when the gateway
    runner is not in this process (typical for cron jobs running
    out-of-process).  The HTTP path is the same one the legacy
    ``_send_homeassistant`` helper used in ``tools/send_message_tool.py``
    before this migration.

    Reads ``HASS_TOKEN`` from ``pconfig.token`` (set by the gateway config
    loader from env) and falls back to the ``HASS_TOKEN`` env var.  Server
    URL comes from ``pconfig.extra["url"]`` (seeded by the env loader in
    ``gateway/config.py``) or the ``HASS_URL`` env var.

    ``thread_id``, ``media_files`` and ``force_document`` are accepted for
    signature parity with other standalone senders.  HA notifications have
    no native threading or attachment model — these arguments are ignored.
    r]   z/aiohttp not installed. Run: pip install aiohttpr7   r'   r(   r   r)   r   Nr   zHHome Assistant standalone send: HASS_URL and HASS_TOKEN must both be setz/api/services/notify/notifyr   r   r   )r   r   r$   rL   rN   )r   r   >   r      zHome Assistant API error (z): Tr   )r^   platformr   z.Timeout sending notification to Home AssistantzHome Assistant send failed: )r   r   r8   r   r   r9   r   rT   rU   rV   r   r   r   rW   r   r\   )pconfigr   r   r   r   r   r7   hass_urlr   r'   r   r   r   r   r   r_   s                   r   _standalone_sendr     s    6 JKKGWb)/RE		% =BIIj"$=EEcJHWgt,K		,0KRRTE5#
 	
 J1
2C"5'**G "W5G=(())3
 
	 
	||Cw|G  4;;j0!%,D8SO  
	 
	 
	 
	 
	 '
 	

	,
	   
	 
	 
	 
	   KIJJ =7s;<<=s  B!G#$.F. E6F. F/E80F3"FE:F-F9E<:F>F. 
E>F. G#FF F F. +F,	F. 5G#6F. 8F:F<F>F.  FF	F	F	FF. F+F" F+'F. .G G#G GG G#G  G#c                 f    ddl m} t        |j                  d      xs dj	                               S )aq  Home Assistant is considered connected when ``HASS_TOKEN`` is set.

    Looks up via ``hermes_cli.gateway.get_env_value`` at call time (not via
    the plugin's own bound import) so tests that patch
    ``gateway_mod.get_env_value`` can suppress ambient ``HASS_TOKEN`` env
    vars.  Matches what the legacy connected-platforms check did before
    this migration.
    r   Nr   r   )hermes_cli.gatewaygatewayr   get_env_valuer   )r   gateway_mods     r   _is_connectedr    s+     -**<8>BEEGHHr   c                     t        |       S )zKFactory wrapper that constructs HomeAssistantAdapter from a PlatformConfig.)r!   )r   s    r   _build_adapterr  )  s    ''r   c                     | j                  ddt        t        t        t        dgdt
        t        j                  dd       y)	u:   Plugin entry point — called by the Hermes plugin system.r   r   r   zpip install aiohttpu   🏠T)rR   labeladapter_factorycheck_fnvalidate_configis_connectedrequired_envinstall_hintstandalone_sender_fnmax_message_lengthemojiallow_update_commandN)register_platformr  r   r   r  r   r!   r   )ctxs    r   registerr  .  sE    &&*""^*
 . 0BB!'  r   r   )%r   rW   r   loggingr   r   r   r   typingr   r   r   r   rT   r   ImportErrorgateway.configr   r	   gateway.platforms.baser
   r   r   r   	getLoggerr   rP   r   r   r   r!   r   listr   r  r  r  r   r   r   <module>r     s      	    + +
 4  
		8	$t 
~ $ N
. N
v  $"& D=D= D=
 }D= $D= D= 
#s(^D=X
IT 
I$(
g  Gs   B! !	B-,B-