o
    REij                     @   s   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mZmZmZ da	da
dZdZdd Zd	d
dddddZG dd dZdddZdddZedkrned e Ze rpe Zee eejeddddd  dS dS dS )u  
WashOS — Miele Move Poller v3
=================================
Hält einen Playwright-Browser offen und pollt die API direkt im Browser.
Die Session bleibt aktiv weil der Browser nie geschlossen wird.

Architektur:
  - Playwright Chromium bleibt im Hintergrund offen
  - API-Calls über page.evaluate(fetch(...)) — nutzt die Browser-Session
  - Kein Cookie-Export nötig — die Session lebt im Browser
  - Bei Session-Ablauf: Seite neu laden → automatischer Re-Login
    N)datetimetimezone	timedelta zhttps://www.miele-move.com<   c                 C   s   t j| d}t j|sttotS z6t|dd}t	|}W d   n1 s*w   Y  |
dp5i }|
dr?|d a|
drH|d aW n	 tyR   Y nw ttoWtS )uo   Lädt Miele-Credentials aus settings.json.
    Gibt True zurück wenn Credentials verfügbar sind, sonst False.zsettings.jsonutf-8encodingNmieleemailpassword)ospathjoinexistsboolMIELE_EMAIL
MIELE_PASSopenjsonloadget	Exception)
output_dirsettings_filefdatacreds r   /app/server/washos-miele.pyload_credentials_from_settings   s"   

r    ABCDEF)000178029100000178029103000178037973000161261920000161261922000161261921c                   @   s   e Zd ZdddZdd Zdd Zdd	 Zd
d Zdd Zde	fddZ
dd ZdefddZdd Zdd ZdefddZdd ZdS ) MielePoller.c                 C   sV   || _ tj|d| _tj|d| _d| _d | _d | _d | _	d | _
i | _|   d S )Nz
miele.jsonzwater_log.jsonF)r   r   r   r   output_filewater_log_file	logged_inbrowsercontextpagepwcycle_state_load_water_log)selfr   r   r   r   __init__4   s   zMielePoller.__init__c                 C   s   z2t j| jr*t| jdd}t|}W d   n1 sw   Y  || _W dS g i d| _W dS  tyB   g i d| _Y dS w )z@Lade bisheriges water_log (falls vorhanden) in self.cycle_state.r   r   N)cyclesmeta)	r   r   r   r0   r   r   r   	water_logr   )r8   r   wlr   r   r   r7   B   s   zMielePoller._load_water_logc              
      s:  zt tjtdd    fdd| jdg D | jd< |  }t tj t	| jd d| jd< || jd< | j
d	 }t|d
dd}tj| j|ddd W d   n1 s\w   Y  zt|| j
 W W dS  ty   ddl}||| j
 Y W dS w  ty } ztd|  W Y d}~dS d}~ww )uC   water_log.json atomar schreiben, alte Einträge (>90 Tage) filtern.Z   )daysc                    s    g | ]}| d d kr|qS )ended_atr   r   ).0ccutoffr   r   
<listcomp>T   s     z/MielePoller._save_water_log.<locals>.<listcomp>r:   )last_updatecycle_countr;   
aggregatesz.tmpwr   r   F   ensure_asciiindentNr   z  [Water] Log-Fehler: )r   nowr   utcr   	isoformatr<   r   _aggregate_waterlenr0   r   r   dumpr   replaceOSErrorshutilmover   print)r8   aggtmpr   rW   er   rD   r   _save_water_logO   s,   "

zMielePoller._save_water_logc              
   C   sD  | j dg }ttj}i }i }i }|D ]}|dd}|dd}|ddp*d}	|dd}
|
r9|
d	d
 nd}||vrH|ddi d||< || d  d7  < t|| d |	 d|| d< ||| d vrtddd|| d |< || d | d  d7  < t|| d | d |	 d|| d | d< |rt||d|	 d||< |d | }t||d|	 d||< qg }t| D ]W\}}|d r|d |d  nd}g }|d  D ]!\}}|	||d |d rt|d |d  dnd|d d q|j
dd dd |	||d |d t|d|d q|||dS )u4   Pro Maschine, pro Programm: Ø Liter, Zyklen, Summe.r:   machine?programwater_litersr   r@   r   N   g        )r^   r:   total_litersprograms   rc   rK   rd   )r:   total_lrf   |)r`   r:   avg_lrf   c                 S   s   | d S )Nr:   r   )xr   r   r   <lambda>   s    z.MielePoller._aggregate_water.<locals>.<lambda>T)keyreverse)r^   r:   rc   avg_liters_per_cycle
by_program)
by_machineby_month_totalby_month_machine)r<   r   r   rO   r   rP   roundsorteditemsappendsort)r8   r:   rO   ro   rp   rq   rC   mprogliterstsmonthrk   machinesdrh   	progs_outpnamepdr   r   r   rR   i   s^   . 

	zMielePoller._aggregate_waterc                 C   sV  | dd}| dd}| d}| d}| dp| d}| d	p&d
}| dp-d
}ttj }	| j |}
|dkr|
rI|
 ddkrhd||	|pOd|pRd|pUd|r[||	ini |p_d|d	| j|< dS | j| }|dur~|| dpxdkr~||d< |dur|| dpdkr||d< |dur|| dpdkr||d< |r||d vr|	|d |< ||d< |dur|n| dd|d< dS |
r!|
 ddkr!||
 dd|
 d|	t|
 dpdd|
 d|
 dt|
 di 	 d}|d dkr| j
dg | td| d|d  d|d   |dd| j|< dS |dd| j|< dS ) z@Wasser- und Programm-Tracking pro Maschine. Erkennt Zyklus-Ende.labelr_   statusunknownwater_volumetemp_currentspinning_currentspinning_targetprogram_namer   program_phaserunningr   )	r   r`   
started_at	max_watermax_tempmax_spinphases_seen
last_water
last_phaseNr   r   r   r   r   r   r`   r   rK   )r^   r`   r   r@   ra   r   r   phasesra   r:   z
  [Water] z Zyklus beendet: u   l · )r   r   )r   r   rO   r   rP   rQ   r6   rr   listkeysr<   
setdefaultru   rY   )r8   devr   r   watertempspinr`   phasenow_isoprevscycler   r   r   _handle_cycle_tracking   sd   


 
"z"MielePoller._handle_cycle_trackingc                 C   sj   ddl m} |  | _| jjjdd| _| jjddddd	| _| j	 | _
| j
d
 | j
d dS )u%   Starte Playwright und öffne Browser.r   sync_playwrightT)headlessi   i  )widthheightzoMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36)viewport
user_agenti`  i_ N)playwright.sync_apir   startr5   chromiumlaunchr2   new_contextr3   new_pager4   set_default_timeoutset_default_navigation_timeout)r8   r   r   r   r   _start_browser   s   zMielePoller._start_browserreturnc              
   C   s:  z | j s	|   td | jjtd dd | jd | jj}td|dd   d|v r>d	|vr>td
 d| _W dS td dD ]*}| j	|}|rn|
 rn|  }td| d |jdd | jd  nqD| jd | jd td | j	d}|r|t | jd | j	d}|r|t | jd | j	d}|r|  n|r|d td | jd | jj}td|dd   d|v sd|v r| d}|rt|dkrdd  |D }	td!|	  d| _W dS td" W d#S  ty }
 ztd$|
  W Y d}
~
d#S d}
~
ww )%u'   Login bei Miele Move über den Browser.z$  [Miele] Navigiere zu /dashboard...
/dashboarddomcontentloaded
wait_untili:  z  [Miele] URL: NP   loginz  [Miele] Bereits eingeloggt!Tz!  [Miele] Cookie-Banner suchen...)zbutton:has-text('Accept all')z#button:has-text('Alle akzeptieren')zbutton:has-text('Akzeptieren')zbutton:has-text('OK')z  [Miele] Cookie: 'z' klicken...)force  z
                document.querySelectorAll(
                    'ngb-modal-window, .modal-backdrop'
                ).forEach(el => el.remove());
            i  z  [Miele] Login-Formular...zainput[type='email'], input[name='username'], input[id*='email'], input[type='text'][name*='mail']i  zinput[type='password']zbutton[type='submit'], input[type='submit'], button:has-text('Login'), button:has-text('Anmelden'), button:has-text('Sign in'), button.gigya-input-submitEnterz  [Miele] Warte auf Login...	dashboard
appliances/appliances/api/devicesr   c                 S   s   g | ]}| d dqS )namer_   rA   rB   r}   r   r   r   rF   J  s    z%MielePoller.login.<locals>.<listcomp>z  [Miele] Login OK! z  [Miele] Login fehlgeschlagenFz  [Miele] Login-Fehler: )r2   r   rY   r4   goto
MIELE_BASEwait_for_timeouturlr1   query_selector
is_visibletext_contentstripclickevaluatefillr   r   press	_api_callrS   r   )r8   r   selbtntxtemail_elpass_el	submit_eltestnamesr\   r   r   r   r      s~   




zMielePoller.loginc              
   C   s   | j sdS z!| j d|}t|tr$d|v r$|d }|dv r!d| _W dS |W S  ty@ } ztd|  W Y d}~dS d}~ww )z'API-Call direkt im Browser via fetch().Na#  
                (path) => fetch(path, {credentials: 'include'})
                    .then(r => {
                        if (!r.ok) return {_error: r.status};
                        return r.json();
                    })
                    .catch(e => ({_error: e.message}))
            _error)i  i.  i  Fz  [Miele] API-Fehler: )r4   r   
isinstancedictr1   r   rY   )r8   r   resulterrr\   r   r   r   r   V  s"   	zMielePoller._api_callc                 C   s  | j sddiS z| jd W n   Y | d}|sddiS g }|D ]}|dd}|dp1i }|d	p8i }d
}|dd}|r_td|}	|	r_t|	dpRdd t|	dp\d }d
}
|dd}|rtd|}	|	rt	t|	dt|	dpdd  d}
|
i d|d|dddt|dd|ddd|ddd|dd|dd|d|d d!|d"d#|d$d%|d&d'|d(d)|d*d+|
d,|d- q#|D ]}| | q|   i }| j D ]2\}}|dd.kr.|d/|d0|d1|d|d2t|d3i  |d4d5||< qttj t|d6d7||  |  |d8S )9z!Einmal alle Miele-Daten abfragen.errornot_logged_inzx
                fetch('/alive', {method:'POST', credentials:'include'})
                    .catch(()=>{})
            r   no_datafabNrr   r`   washingNremainingTimezPT(?:(\d+)H)?(?:(\d+)M)?re   r   r   rK   operatingTimezPT(\d+)H(?:(\d+)M)?fab_nrr   r_   r   	tech_typetechTyper   r   r   r   	phaseNameremaining_minr   temperatureCurrentr   spinningSpeedCurrentr   spinningSpeedTargetr   waterVolumeload_weight
loadWeight
max_weight	maxWeightoperating_hours	synced_atsyncedAtr   r   r   r   r   r   r   )current_waterr   r   r`   r   r   r   
miele_move)rG   device_countsource)r;   devicesalertsprogram_statslive_water_cycles)r1   r4   r   r   r   rematchintgrouprr   ru   
DEVICE_MAPr   r]   r6   rt   r   r   r   rO   r   rP   rQ   rS   _fetch_alerts_fetch_program_stats)r8   devices_rawr   r}   r   rx   r   r   rtrw   op_hoursotr   
live_waterlblstater   r   r   	poll_oncen  s   



	





zMielePoller.poll_oncec                 C   4   zt t }| jd|}|pg W S    g  Y S )z0Letzte 5 abgebrochene/fehlgeschlagene Programme.ad  
                (fabs) => {
                    var promises = fabs.map(fab =>
                        fetch('/appliances/api/devices/program/'
                            + 'executions?fabNr=' + fab
                            + '&size=3&page=0&status=cancelled',
                            {credentials: 'include'})
                        .then(r => r.ok ? r.json() : {items:[]})
                        .catch(() => ({items: []}))
                    );
                    return Promise.all(promises).then(results => {
                        var all = [];
                        results.forEach(r => {
                            (r.items || []).forEach(item => {
                                all.push({
                                    machine: item.device
                                        ? item.device.name : '?',
                                    label: item.device
                                        ? item.device.techType : '?',
                                    status: item.status,
                                    program: item.program
                                        ? item.program.name : '?',
                                    started: item.startedAt,
                                    stopped: item.stoppedAt,
                                    duration: item.duration,
                                });
                            });
                        });
                        all.sort((a, b) =>
                            b.started.localeCompare(a.started));
                        return all.slice(0, 5);
                    });
                }
            r   r   r   r4   r   r8   fabsr   r   r   r   r     s   !
"zMielePoller._fetch_alertsc                 C   r  )u2   Programmrangliste über alle Maschinen aggregiert.a2  
                (fabs) => {
                    var promises = fabs.map(fab =>
                        fetch('/appliances/api/devices/program/'
                            + 'executions/status/aggregation'
                            + '?fabNr=' + fab,
                            {credentials: 'include'})
                        .then(r => r.ok ? r.json() : [])
                        .catch(() => [])
                    );
                    return Promise.all(promises).then(results => {
                        var agg = {};
                        results.forEach(progs => {
                            (progs || []).forEach(p => {
                                var name = p.program
                                    ? p.program.name : '?';
                                var st = p.status || {};
                                var total = (st.completed || 0)
                                    + (st.cancelled || 0)
                                    + (st.failure || 0)
                                    + (st.unknown || 0);
                                if (!agg[name]) {
                                    agg[name] = {
                                        name: name,
                                        total: 0,
                                        ok: 0,
                                        cancelled: 0,
                                        failure: 0
                                    };
                                }
                                agg[name].total += total;
                                agg[name].ok += st.completed || 0;
                                agg[name].cancelled +=
                                    st.cancelled || 0;
                                agg[name].failure +=
                                    st.failure || 0;
                            });
                        });
                        var list = Object.values(agg);
                        list.sort((a, b) => b.total - a.total);
                        return list.slice(0, 15);
                    });
                }
            r  r	  r   r   r   r     s   +
,z MielePoller._fetch_program_statsr   c                 C   s   t | jddd}tj||ddd W d   n1 sw   Y  t d}d	d
 |dg D }|rQddd |D }t	d| dt
| d| d dS t
|dg }t	d| d| d dS )zSchreibe miele.json.rJ   r   r   FrK   rL   Nz%H:%M:%Sc                 S   s   g | ]
}|d  dkr|qS )r   r   r   r   r   r   r   rF   6  s
    z,MielePoller.write_output.<locals>.<listcomp>r   z, c                 s   s(    | ]}|d  d t |d  V  qdS )r   :r   N)strr   r   r   r   	<genexpr>;  s
    
z+MielePoller.write_output.<locals>.<genexpr>[z
] MIELE | z
 laufend ()z] MIELE | Alle z Maschinen standby)r   r/   r   rT   r   rO   strftimer   r   rY   rS   )r8   r   r   rz   r   progstotalr   r   r   write_output1  s   

$zMielePoller.write_outputc              
   C   sh  t d zddlm} W n ty"   t d t d t d Y dS w |  s8t d t d	 td
 |  S t dt d d}	 zM| 	 }d|vrT| 
| d}n;|d7 }|dkrt d z$| jjtd dd | jd | jj}d|v r~|   nd| _d}W n
   |   d}Y W n ty } zt d|  |d7 }W Y d}~nd}~ww tt qC)z(Endlos-Polling mit persistentem Browser.z  [Miele] Starte Browser...r   r   z'  [Miele] playwright nicht installiert!z  pip install playwrightz'  python -m playwright install chromiumNz&  [Miele] Erster Login fehlgeschlagen.z  [Miele] Retry in 5 Min...i,  z"  [Miele] Polling gestartet (alle zs)Tr   re      z(  [Miele] 3x fehlgeschlagen, Re-Login...r   r   r   i'  r   z  [Miele] Fehler: )rY   r   r   ImportErrorr   timesleep
run_pollerPOLL_INTERVALr  r  r4   r   r   r   r   r1   r   )r8   r   
fail_countr   r   r\   r   r   r   r  D  s^   



zMielePoller.run_pollerNr.   )__name__
__module____qualname__r9   r7   r]   rR   r   r   r   r   r   r   r  r   r   r  r  r   r   r   r   r-   3   s    
<F\e*4r-   r.   c                 C   sH   t | }|std td dS t| d}tj|jddd}|  |S )u   Startet den Miele-Poller als Daemon-Thread.
    Wenn keine Credentials vorhanden: gibt None zurück und meldet 
    dies in der Konsole — Thread wird nicht gestartet.uA     [Miele] Keine Credentials in settings.json — Poller pausiert.z/  [Miele] Bitte in den Einstellungen eintragen.Nr   Tr-   )targetdaemonr   )r    rY   r-   	threadingThreadr  r   )r   	has_credspollertr   r   r   start_miele_threadx  s   
r'  c                 C   s  t |  t| d}zzi| }|s4W W z|jr|j  |jr'|j  W dS W dS  ty3   Y dS w z| }t	|p=i 
dg }W n tyO   d}Y nw dd|fW W z|jr_|j  |jri|j  W S W S  tys   Y S w  ty } z+dt|dfW  Y d}~W z|jr|j  |jr|j  W S W S  ty   Y S w d}~ww z|jr|j  |jr|j  W w W w  ty   Y w w )u   Einmaliger Login-Test für /api/miele/test.
    Lädt Credentials aus settings.json und versucht Login — ohne Poll.
    Gibt (ok: bool, message: str, devices: int|None) zurück.r  )Fu0   Login fehlgeschlagen — E-Mail/Passwort prüfenNr   NT	VerbundenF)r    r-   r   r2   closer5   stopr   r  rS   r   r  )r   r%  okr   nr\   r   r   r   try_login_with_settings  sj   




r-  __main__u%   Miele Move Poller — Standalone-TestrK   F)rN   rM   r   r  )__doc__r   r   r  r"  r   r   r   r   r   r   r   r  r    r   r-   r'  r-  r  rY   r%  r   r  r   r  dumpsr   r   r   r   <module>   s6   (    
I

 