o
    REi                     @   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Z	da
dadZdZdZd	d
 Zg dZddgZddgZdddZdddddddZdddddddd ZG d!d" d"Zd#d$ Zd5d&d'Zd6d(d)Zd7d+d,Zd7d-d.Zed/kred0 e Ze re  Z!e"e! eej#e!d1d2d3dd4  dS dS dS )8u  
WashOS — Seko Dosierpumpen-Poller v1
========================================
Pollt die Seko Web API für Waschmittel-Verbrauchsdaten.

Architektur:
  - Einfache HTTP-Session (requests) mit Cookie-Auth
  - Login über POST /login/checkUser
  - Chemikalien-Verbrauch über POST /application/ctrl_get_chemical_usage
  - Gerätestatus über GET /devices/ctrl_get_list
  - Ausgabe: seko.json (wird vom Dashboard gelesen)

Starten: Wird automatisch von washos-server.py gestartet.
    N)datetimetimezone	timedeltazhttps://gbl2146seko.sekoweb.com GBL2146SEKOz$75d34340-26fd-4a8b-ab2e-84d408d89b61i,  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 )un   Lädt Seko-Credentials aus settings.json.
    Gibt True zurück wenn Credentials verfügbar sind, sonst False.settings.jsonutf-8encodingNsekoemailpassword)ospathjoinexistsbool	SEKO_USER	SEKO_PASSopenjsonloadget	Exception)
output_dirsettings_filefdatacreds r   /app/server/washos-seko.pyload_credentials_from_settings   s"   

r!   )z<012500003B9D_Ga6c3579c61134d31b8b1b82ea8e31136#1#A Small (1)z<012500003B9D_Ga6c3579c61134d31b8b1b82ea8e31136#2#B Small (2)z=012500003B9D_Ga6c3579c61134d31b8b1b82ea8e31136#3#C Medium (3)z<012500003B90_G2d00cb1d22fd42dba6554472c83eaaf3#1#D Large (1)z<012500003B90_G2d00cb1d22fd42dba6554472c83eaaf3#2#E Large (2)z<012500003B90_G2d00cb1d22fd42dba6554472c83eaaf3#3#F Large (3)012500003B9D012500003B90.012500003B9D_Ga6c3579c61134d31b8b1b82ea8e31136.012500003B90_G2d00cb1d22fd42dba6554472c83eaaf3zPumpe 1 (Small-Block)zPumpe 2 (Large-Block))r$   r%   zA SmallzB SmallzC MediumzD LargezE LargezF Large)r                  g      @g@g@g@g@g)@)ENERGYMEGASOFTSANYPLUSTOTALzSG CLEANz	NSG-CLEANPROOFINGc                   @   s   e Zd Zd.ddZdefddZdefddZd/defddZ	d0de
dedefddZdefddZdefddZd1dedefddZdZdedefddZdedefdd Z	
	
d2d!ed"edefd#d$Zd%efd&d'Zd3d)efd*d+Zd,d- Zd
S )4
SekoPoller.c                 C   sN   || _ tj|d| _tj|d| _t | _| jj	
ddd d| _d S )Nz	seko.jsonzseko_events_archive.jsonz
WashOS/1.0z application/json, text/html, */*)z
User-AgentAcceptF)r   r   r   r   output_filearchive_filerequestsSessionsessionheadersupdate	logged_in)selfr   r   r   r    __init__i   s   

zSekoPoller.__init__returnc              
   C   s   zU| j jt dttdddd}|jdkr%d|jvr%d| _td W dS |jdkrK| j j	t d	t
 d
d}|jdkrKd|jv rKd| _td W dS td|j  W dS  tyo } ztd|  W Y d}~dS d}~ww )zLogin bei Seko Web.z/login/checkUser)usernamer   -   T)r   timeoutallow_redirects   loginz  [Seko] Login OK/devices/ctrl_get_list/
   r@   retCodez   [Seko] Login OK (via redirect)z$  [Seko] Login fehlgeschlagen: HTTP Fz  [Seko] Login-Fehler: N)r7   post	SEKO_BASEr   r   status_codeurlr:   printr   
SEKO_OWNERtextr   )r;   rtester   r   r    rC   u   s6   
zSekoPoller.loginc              
   C   s   z2| j jt dt dd}|jdkr#| }|dr#|dg W S |jdks-|jdkr0d	| _g W S  tyM } ztd
|  g W  Y d}~S d}~ww )u   Geräteliste abrufen.rD   r?   rF   rB   rG   table  .  Fu     [Seko] Geräte-Fehler: N)	r7   r   rI   rM   rJ   r   r:   r   rL   )r;   rO   r   rQ   r   r   r    _get_devices   s"   

zSekoPoller._get_devicesmonthNc           
   
   C   sV  t  }|r|r|}|}nB|dkr|jdddd}|}n3|dkr*|tdd }|}n%|dkr8|tdd }|}n|d	krF|td
d }|}n	|tdd }|}td|rUdn|t| t| tg ti d	}z(| j	j
t dt |dd}|jdkr| W S |jdks|jdkrd| _i W S  ty }	 ztd|	  i W  Y d}	~	S d}	~	ww )zChemikalien-Verbrauch abrufen.todayr   )hourminutesecondweek   )daysrV      yearm  rR   custom)	applicationIDtype	rangeType	startDateendDatewasherowner
devicesIDS	customersz%/application/ctrl_get_chemical_usage/r   r@   rB   rS   rT   Fz   [Seko] Chemical-Usage-Fehler: N)r   nowreplacer   SEKO_APP_IDint	timestamp
WASHER_MAP
DEVICE_IDSr7   rH   rI   rM   rJ   r   r:   r   rL   )
r;   
range_type
start_dateend_daterl   startendparamsrO   rQ   r   r   r    _get_chemical_usage   sX   




zSekoPoller._get_chemical_usage   gid
hours_backc              
   C   s   t t  }||d  }||ttt|dd}z&| jjt	 d|dd}|j
dkr/| W S |j
dks9|j
d	kr<d
| _i W S  ty` } ztd|dd  d|  i W  Y d}~S d}~ww )u  NEU: Dosier-Events pro Pumpe (GID) abrufen.

        Liefert für jede Chemikalie eine Event-Liste mit:
          - T (timestamp ms), DQ (dosed quantity ml), DT (duration s),
            FR (flow rate ml/min), DC (device channel 1-3), CV, RV

        Der Endpoint verlangt pro GID einen separaten Call (die Seko-Web-UI
        macht das auch so — ein Call pro Pumpe).

        hours_back: Standard 24h (für Zyklus-Korrelation reicht das; längere
        Zeiträume bringen kilobyteweise Daten pro Poll).  Waschsalon Nord GmbH	startTimeendTimerg   ownerIDrb   GIDinstallationSiteName /application/ctrl_getflowrecordsr^   rk   rB   rS   rT   Fz  [Seko] Flow-Records-Fehler (N   u   …): )ro   r   rl   rp   rq   rM   rn   r7   rH   rI   rJ   r   r:   r   rL   )r;   r{   r|   now_sstart_srx   rO   rQ   r   r   r    _get_flow_records   s4   


zSekoPoller._get_flow_recordsc           8      C   sL  | j sddiS |  }g }|D ]@}|dd}ddl}|dd| }|||dd|d	dd
|ddv r;dnd|dd|dd|ddd q| d}| d}| d}	tddd}
t	 }||
 j
pod}| jd|
|d}dd }||}||}||	}||}tdd |D }tdd |D }tdd |D }tdd |D }tdd |D }td d |D }|dkrt|| d!nd}i }|D ]}|d"i  D ]\}}t||d| d#||< qqg }i }tD ]3} t| | dd$ }!dd%t d&||!< | j| d'd(}"|"sq|"d)pi d*p$i }#|#| p,i }$|$ D ]\}%}&t|&ts=q1|&d+|%}'t|% t|' d}(|&d,pXi })|) D ]\}*}+t|+tsiq]|+d-d},|,ry|,d.d nd/}-|+d0pi }.|. D ]\}/}0t|0tsq|0D ]}1t|1tsq|1d1p|1d2pd}2|2du s|2sqt|1d3}t|1d4}3t|1d5}4||2tj|2d6 tjd7 | |!|*|,|/|-|%|'t|3d#t|d8|4tt|1d9d|(r t|3d: |( d;ndd< ||! d=  d7  < ||! d>  |7  < ||! d? |' qqq]q1q|jd@dA dB |D ]}5t ||5 d? ||5 d?< t||5 d> d#||5 d>< q5| !|\}6}7t	tj dCt"t#|dD||6|7dE|t|dt|dt|dt|d!t|d!t|d!|t#|tdFd |D t#||dG||||||d't#||dHdI
S )Jz Einmal alle Seko-Daten abfragen.errornot_logged_indeviceIDr   r   Nz<[^>]+>systemModelnamezdevice-onlineonlineofflinefirmwarebuild
LASTUPDATE)	device_idmodelr   statusr   r   last_updaterW   rV   r_   i  r&   ra   )rt   ru   c                 S   s  |  di }g }| D ]s\}}| dg }i }t|D ]\}}t |d|d  }	t|d||	< qt| ddd}
t | t | dd	 d}t| d
dd}|r`t|
| dn|}||| d|| ddt| dd|
||d|d	 q|j	dd dd |S )z&Parse die Seko chemical_usage Antwort.values	totalListz	Maschine r&   r(   totalr   r   r   costr'   runningTimeEUR)	keyr   runtime_secondsruntime_formattedtotal_litrescost_eurprice_per_litre	cost_unitper_machinec                 S      | d S )Nr   r   xr   r   r    <lambda>E      z;SekoPoller.poll_once.<locals>.parse_usage.<locals>.<lambda>Tr   reverse)
r   items	enumerateMACHINE_NAMESroundCHEMICAL_PRICESupperappendformat_durationsort)r   r   	chemicalsr   v
total_listr   ilitresmachinetotal_lprice_per_l	seko_costactual_costr   r   r    parse_usage#  s@   



z)SekoPoller.poll_once.<locals>.parse_usagec                 s       | ]}|d  V  qdS r   Nr   .0cr   r   r    	<genexpr>N      
z'SekoPoller.poll_once.<locals>.<genexpr>c                 s   r   r   r   r   r   r   r    r   P  r   c                 s   r   r   r   r   r   r   r    r   R  r   c                 s   r   r   Nr   r   r   r   r    r   T  r   c                 s   r   r   r   r   r   r   r    r   V  r   c                 s   r   r   r   r   r   r   r    r   X  r   r'   r   r(   r           )event_countr   r   rz   )r|   
devicedataCHEMICAL_FLOWCNWASWN ?CHSTrp   TQDQDT  tz   FR     @@r)   ts_msts_isopump_gid	pump_name
washer_keywasher_namechannel_setr   chemical_keychemical_namemlr   
duration_s	flow_rater   r   r   r   c                 S   r   )Nr   r   rQ   r   r   r    r     r   z&SekoPoller.poll_once.<locals>.<lambda>r   seko_webz
2026-01-01)r   sourceowner_iddevice_count	ref_startref_daysarchive_total_eventsarchive_new_events_this_pollc                 s   s     | ]}|d  dkrdV  qdS )r   r   r&   Nr   r   dr   r   r    r         )total_litres_monthtotal_litres_yeartotal_litres_reftotal_cost_monthtotal_cost_yeartotal_cost_refcost_per_litrechemicals_countpumps_onlinepumps_totalr   )window_hourstotal_eventsby_pump)
metadevicessummarychemicals_todaychemicals_monthchemicals_yearchemicals_refmachine_totals_refdosing_events_24hdosing_events_meta)$r:   rU   r   resubstripr   ry   r   rl   r]   sumr   r   GIDS
PUMP_NAMESsetr   
isinstancedictr   r   splitlist_safe_float	_safe_intfromtimestampr   utc	isoformataddr   sorted_merge_to_archiverM   len)8r;   devices_rawr  r   didr  	did_cleanusage_todayusage_month
usage_yearr   ref_endr   	usage_refr   r  r  r	  r
  r   r   r   r   r   r   r   machine_totalsr   mr   dosing_eventsevents_by_pumpr{   r   	flow_data	chem_flow
gid_bucketchem_key	chem_data	chem_namer   was_dictwas_keywas_datawnr   chschs_keyeventsevr   r   r   pnamearchive_totalarchive_addedr   r   r    	poll_once   sR  








%



8

zSekoPoller.poll_oncec              
   C   s   t j| js	g S zDt| jdd}t|}W d   n1 s!w   Y  |dp,i }|dd}|| jk rGt	d| d| j d	 g W S |d
g W S  t
yh } zt	d|  g W  Y d}~S d}~ww )u
  Lade das persistente Event-Archiv. Liefert eine Liste von Events.

        Schema-Check: Wenn die Archiv-Version älter ist als die aktuelle
        Schema-Version (= Dedup-Logik hat sich geändert), wird das Archiv
        verworfen und beim Backfill neu aufgebaut.r   r	   Nr  schema_versionr&   z"  [Seko] Archiv-Schema veraltet (vz < vu.   ) — Archiv wird verworfen und neu aufgebaut.r:  z  [Seko] Archiv-Lade-Fehler: )r   r   r   r4   r   r   r   r   ARCHIVE_SCHEMA_VERSIONrL   r   )r;   r   r   r  stored_versionrQ   r   r   r    _load_archive  s.   
zSekoPoller._load_archiver:  r  c              
   C   s   zN|pi |d}| j d }t|ddd}tj||ddd W d	   n1 s(w   Y  zt|| j  W W d	S  tyN   d
d	l}||| j  Y W d	S w  t	yh } zt
d|  W Y d	}~d	S d	}~ww )z!Schreibe das Event-Archiv atomar.)r  r:  .tmpwr   r	   Fr&   ensure_asciiindentNr   z   [Seko] Archiv-Schreib-Fehler: )r4   r   r   dumpr   rm   OSErrorshutilmover   rL   )r;   r:  r  payloadtmpr   rK  rQ   r   r   r    _save_archive
  s$   
zSekoPoller._save_archiver'   
new_eventsc              	   C   s  |   }i }|D ]#}|d|df}||}|r'|dd|ddkr+|||< qt|dd d}d}|D ]Q}|d|df}	||	}|r|t|ddd	t|ddd	kox|d
|d
koxt|dddt|dddk}
|
r|q8|| |||	< |d7 }q8|jdd d | jtt	j
 t||r|d d nd|r|d d nd|r|d dnd|r|d dndd}| || t||fS )u  Merge neue Events ins Archiv mit Payload-Dedup.

        WICHTIG: Die Seko-API (ctrl_getflowrecords) liefert bei jedem Poll
        den *zuletzt gemessenen Dosiervorgang* pro Pumpe/Maschine/Chemikalie.
        Wenn seit dem letzten Poll nichts Neues dosiert wurde, kommt der
        gleiche Event mit neuem Timestamp. Naives Dedup auf `ts_ms` würde
        jeden Poll als neuen Event zählen → Mehrfachzählung.

        Korrekte Logik: Dedup auf (machine, chemical, ml, duration_s,
        flow_rate). Wenn der gleiche Dosierwert bei identischer Dauer und
        Flow-Rate erneut kommt, ist's derselbe Vorgang wiederholt.

        Edge-Case: Zwei echt aufeinanderfolgende Dosierungen mit exakt
        identischen Werten werden als eine gezählt — bei ml auf 3 Nachkomma-
        stellen + Flow-Rate (0.1 ml/min Präzision) ist das praktisch
        ausgeschlossen; Flow-Rate variiert durch Druckschwankungen.

        Liefert (total_count, added_count) zurück.r   r   r   r   c                 S      |  ddS Nr   r   r   r   r   r   r    r   ?      z.SekoPoller._merge_to_archive.<locals>.<lambda>r   r   r(   r   r   r&   c                 S   rQ  rR  rS  r   r   r   r    r   S  rT  Nr   )r@  r   r  earliest_ts_mslatest_ts_msearliest_iso
latest_iso)rC  r   r  r   r   r   rA  r   rl   r   r  r  r!  rO  )r;   rP  existing
last_by_mcr;  r   lastnew_events_sortedaddedmc_keysame_payloadr  r   r   r    r   !  sF   

$"

	zSekoPoller._merge_to_archive	seko_datac           (      C   s  | dpg }|sdg i dS tj| jd}tj|s#dg i dS zt|dd}t|}W d   n1 s:w   Y  W n t	y[ } zd	| g i dW  Y d}~S d}~ww | d
g }t
  d d }g }	|D ]%}
zt
|
d dd}| d |kr|	|
 W qp t	y   Y qpw |	sdg i t|dS i }|D ]}| dd}||g | qd}d}g }|	D ]}z t
|d dd d }t
|d dd d }W n	 t	y   Y qw | dd}| |g }g }|D ]}| dd}|| |  kr|| krn q|| qi }d}d}|D ]R}| dp1| dd}||vr?dddd||< || d  | dd7  < || d  d7  < || d   | d d7  < || dd7 }|| d d7 }q$g }t| d!d" d#D ]\}}||t|d d|d t|d  d$d% q||| d&d| d| d| d'| d(t|t|dt|d$|d)
 qi }|D ]}|d* dkrאq|d& pd} | d'}!|!r|  d+|! d,n| }"|"|vr| |!dddi t d-||"< ||" }#|#d.  d7  < |#d/  |d/ 7  < |#d0  |d0 7  < |#d1 |d  |d2 D ]1}$|$d3 }||#d4 vrEddd5|#d4 |< |#d4 | d/  |$d 7  < |#d4 | d.  d7  < q/q| D ]o\}"}#|#d. pqd}%t|#d/ |% d|#d6< t|#d0 |% d$|#d7< t|#d/ d|#d/< t|#d0 d$|#d0< t|#d1 |#d1< g }&t|#d4  d8d" d#D ]\}}'|&|t|'d/ dt|'d/ |% d|'d. d9 q|&|#d2< |#d4= qgd:d;t|	td<d= |D t|d>d" d?d@|dAS )Bu  Korreliert Seko-Dosier-Events mit Miele-Zyklen.

        Ansatz:
          1. water_log.json laden (enthält alle Miele-Zyklen mit
             machine/program/started_at/ended_at/max_temp)
          2. Für jeden Zyklus alle Seko-Events der gleichen Maschine im
             Zeitfenster [started_at - 2 min, ended_at + 5 min] sammeln
          3. Aggregat pro Zyklus → Liste Chemikalien mit ml + €
          4. Aggregat pro (Programm + Temperatur) → Ø + Spannbreite

        Das Zeitfenster ist asymmetrisch: -2 min (Dosierpumpen starten manchmal
        kurz vor Miele-Zyklus-Erkennung), +5 min (Miele meldet Ende oft bevor
        die letzte Spül-Dosierung vorbei ist).
        r  	no_events)r   cycles
by_programwater_log.jsonno_miele_datar   r	   Nzwater_log_error: rc  im r   ended_atZ+00:00no_recent_cycles)r   rc  rd  miele_cycles_totalr   r     
started_atr   r   r   r   r   )r   r:  r   r   r:  r&   r   c                 S      | d d  S )Nr&   r   r   r   r   r   r    r         z1SekoPoller.correlate_with_miele.<locals>.<lambda>r   r)   )chemicalr   r:  r   programmax_tempwater_liters)
r   rr  rn  rg  rs  rt  r   total_mltotal_cost_eurr   r   z @ u   °C)rr  rs  cycle_countru  rv  by_chemicalmachines_usedrw  ru  rv  ry  r   rq  rx  )ru  rw  avg_ml_per_cycleavg_cost_per_cyclec                 S   ro  )Nr&   ru  r   r   r   r   r    r     rp  )rq  ru  rz  cycles_with_thisokrz   c                 s   s     | ]}|d  dkrdV  qdS )r   r   r&   Nr   r   r   r   r    r     r   z2SekoPoller.correlate_with_miele.<locals>.<genexpr>c                 S      | d pdS Nrg  r   r   )r   r   r   r    r     rT  Tr   )r   r  miele_cycles_in_windowmiele_cycles_correlatedrc  rd  )r   r   r   r   r   r   r   r   r   r   r   rl   rp   fromisoformatrm   r   r!  
setdefaultr  r   r   r  r  r  )(r;   ra  r:  water_log_filer   	water_logrQ   
all_cycles	cutoff_msrelevant_cyclesr   endedevents_by_machiner;  r+  PRE_BUFFER_MSPOST_BUFFER_MScorrelated_cyclescycstartedr   machine_eventsmatching_eventstsby_chemru  
total_costchem
chems_listr   rd  ccprogtempr   aggchn	chems_outcdr   r   r    correlate_with_mielea  s4  

$






	
zSekoPoller.correlate_with_mielesettingsaggregated_datac           M         sp  |pi }| dd}| dpddddddd}| dp"d	d
dd}| dp)i }| dd}zt|dd d W n ty[   t  d d td  }Y nw | 	 }fdd|D }|spd|dS g }	t
j| jd}
t
j|
rzIt|
dd}t|}W d   n1 sw   Y  | dg D ]%}zt|d dd d kr|	| W q ty   Y qw W n	 ty   Y nw i }|D ]}| dd}||g | qd d! g }|	D ]}z t|d" dd d t|d dd d W n
 ty   Y qw | dd}| |g } fd#d|D }|s:qi }d$}d$}|D ]G}| d%pO| d&d}||vr\d$d$d'||< || d(  | d(d)7  < || d*  | d*d)7  < || d(d)7 }|| d*d)7 }qB||| |d+| d,d| d"| d| d-| d.t|d/t|d0d1d2 | D d3 qi }|D ]}|d, }|d4 }| d5| }||vr||d)d$d$d$i i t g g d6||< || }|d7  d/7  < |d8  |d8 7  < |d9  |d9 7  < |d:  | d.pd)7  < |d; |d  |d< |d8  |d= |d9  |d>  D ]%\}} |d?  |d)| d(  |d? |< |d@  |d)| d*  |d@ |< q7q| D ]\}}|d7 pmd/}!t|d8 |! d/|dA< t|d9 |! d0|dB< t|d: |! d/|dC< |!d/kr|dA tfdDdE|d< D |! }"t|"dF d/|dG< nd)|dG< | |d4 d)}#|#|dH< t|#|dB  d0|dI< |#d)krt|dI |# dJ d/nd)|dK< t|d8 d/|d8< t|d9 dL|d9< t|d: d/|d:< t|d; |d;< g }$t|d?  dMdN dOD ] \}}|$|t|d/t|d@  |d)d0t||! d/dP q|$|dQ< |d?= |d@= |d<= |d== qci }%|D ]K}|d, }||%vr`|d)d$d$t t dR|%|< |%| }&|&d7  d/7  < |&d8  |d8 7  < |&d9  |d9 7  < |&d; |d  |&dS |d4  qH|% D ]D\}}&|&d7 pd/}!t|&d8 |! d/|&dA< t|&d9 |! d0|&dB< t|&d8 d/|&d8< t|&d9 dL|&d9< t|&d; |&d;< t|&dS |&dS< qd)dTlm}' |'dUdN }(|D ]b}| dVd)})|)d)krqt|)d dW}*|(|* dX  d/7  < |(|* d8  | d(d)7  < |(|* d9  | d*d)7  < | d%p5| d&d}|(|* dY  |d)| d(d) |(|* dY |< qg }+t|( D ]'}*|(|* },|+|*|,dX t|,d8 d/t|,d9 d0dZd2 |,dY  D d[ qVt|+}-|+td\|- d }.|.rtd]dE |.D t|. }/td^dE |.D t|. }0nd)}/d)}0t|.t|/dLt|0d/t|/d_ dLt|0d_ d d/t|/d` dLt|0d` d d/da}1g }2|D ]^}|d,  d5|d4  }| |}|r|d7 dbk rq|dA }3|3d)krq|d8 }4|4|3 |3 dJ }5| |d, |}6t |5|6kr9|2|d |d, |d |4|3t|5d/|d9 |6dc q|2j!dddN dedf tdgdE |D }7tdhdE |D }8t|}9|+rc|+d) di nd}:|+rn|+dj di nd};|-}<d}=|r| dkp~g }>| dlpg }?| dmpg }@| dnpg }A| dopi }Bi }C|>D ]~}| dpi pi }D| dqd)pd)}#|D D ]d\}E}Ft"|Ft#t$fr|Fd)krΐq|E%drd) }G|G|Cvr|Gdr|E%drd/d d$d$i ds|C|G< |C|G dt  |F7  < |C|G d9  |F|# 7  < t|Fdbt|F|# dLdu|C|G d> | dv| dw< qq|C D ]\}G},t|,dt db|,dt< t|,d9 dL|,d9< q&dxd |>D }H|B dyd)}I|B dzd)}J|B d{d)}K|B d|d)}Ld}d~t|IdLt|JdLt|KdLt|LdL|B dd)d|@|Ht&|C' |B d|B dd)|B dd)d	}=d||:|;|<|=|9t|7d/t|7d dLt|8dLt|dd|1||%|+|2|||||ddS )u4  Große Aggregation über das gesamte Event-Archiv.

        Nutzt das persistente seko_events_archive.json + water_log.json
        und produziert:
          - Programm × Maschinengröße (Small/Medium/Large) Aggregate
          - Verbrauch im Zeitverlauf (täglich)
          - Hochrechnung Monat/Jahr
          - Wirtschaftlichkeit (Umsatz − Chemie pro Programm)
          - Anomalien (Zyklen die >X% vom Programm-Schnitt abweichen)

        WICHTIG: Die Event-basierten Aggregationen sind UNVOLLSTÄNDIG —
        der Seko-Event-Endpoint (ctrl_getflowrecords) liefert historisch
        nur ~30% der tatsächlichen Dosierungen. Die Haupt-KPIs
        (Monatskosten, Hochrechnung) kommen deshalb aus aggregated_data
        (= chemicals_today/month/year aus ctrl_get_chemical_usage).
        Die Event-Auswertung ist für relative Programm-Vergleiche und
        Anomalien weiter nützlich.

        aggregated_data: Das komplette seko.json-Data-Dict mit
        chemicals_today, chemicals_month, chemicals_year, summary.

        settings: Optional dict mit:
          - 'pump_start_date': ISO-Datum, ab wann ausgewertet wird
          - 'anomaly_thresholds': dict {chemical_or_program: percent}
          - 'machine_size_map': {machine: 'small'|'medium'|'large'}
          - 'machine_prices': {size: euro_per_wash}
        pump_start_datez
2026-03-21machine_size_mapsmallmediumlarge)ABCDEFmachine_pricesg      @g      @g       @)r  r  r  anomaly_thresholdsdefault_anomaly_pct2   rh  ri  r   i $ c                    s    g | ]}| d d kr|qS r   r   rS  r   rQ   )pump_start_msr   r    
<listcomp>M  s    z;SekoPoller.compute_archive_aggregations.<locals>.<listcomp>no_archive_data)r   
pump_startre  r   r	   Nrc  rg  r   r   rl  rm  rn  c                    s8   g | ]} | d d  kr  krn n|qS r  rS  )r   r;  )r  r  r  r  r   r    r  }  s    
r   r   r   r   r   r   r   r   unknownrr  rs  rt  r&   r)   c                 S   s0   i | ]\}}|t |d  dt |d ddqS )r   r&   r   r)   r  r   r   kr   r   r   r    
<dictcomp>  s
    
z;SekoPoller.compute_archive_aggregations.<locals>.<dictcomp>)r   machine_sizerr  rn  rg  rs  rt  	ts_ms_endru  rv  r  r  __)rr  r  rw  ru  rv  total_water_lby_chem_total_mlby_chem_total_costmachinesml_per_cycle_listcost_per_cycle_listrw  ru  rv  r  r  r  r  r  r  r  rz  r{  avg_water_l_per_cyclec                 3   s    | ]	}|  d  V  qdS )r'   Nr   )r   r   )meanr   r    r         z:SekoPoller.compute_archive_aggregations.<locals>.<genexpr>g      ?std_ml_per_cycleprice_per_washmargin_per_cycled   
margin_pctr'   c                 S   s
   | d  S )Nr&   r   r   r   r   r    r     s   
 z9SekoPoller.compute_archive_aggregations.<locals>.<lambda>r   )rq  ru  rv  rz  r   )rr  rw  ru  rv  r  sizesr  )defaultdictc                   S   s   dddi dS )Nr   r   )r:  ru  rv  
by_chem_mlr   r   r   r   r    r     s    r   %Y-%m-%dr:  r  c                 S   s   i | ]
\}}|t |d qS )r&   r  r  r   r   r    r  $  s    )dater:  ru  rv  r     c                 s   r   )rv  Nr   r   r   r   r    r   ,      c                 s   r   )ru  Nr   r   r   r   r    r   -  r  r^   r`   )based_on_daysavg_daily_cost_euravg_daily_mlmonthly_cost_eurmonthly_litresyearly_cost_euryearly_litresr(   )r   rr  rg  	actual_mlavg_mldeviation_pctr   threshold_pctc                 S   r~  r  r   )ar   r   r    r   X  rT  Tr   c                 s       | ]	}| d dV  qdS )r   r   NrS  r  r   r   r    r   [  r  c                 s   r  )r   r   NrS  r  r   r   r    r   \  r  r  rU  r  r	  r  r
  r  r   r   r   )r   
size_labelr   rv  r  r   )r   r   r   r   c                 S   sh   g | ]0}| d p
ddkr| d| dt| d ddt| ddd| dd| ddd	qS )
r   r   r   r   r(   r   r'   r   r   )r   r   r   r   r   r   )r   r   r   r   r   r    r    s    	

	r   r   r   r   seko_ctrl_get_chemical_usageu^   Diese Werte kommen vom offiziellen Seko-Aggregat-Endpoint und stimmen mit deiner BWA überein.r   )r   r   r   r   avg_cost_per_litrer   r   r   )	r   noter  r  r  per_machine_monthr   r   r   r}  ul   Event-basierte Zahlen: unvollständig (~30% Abdeckung). Für absolute Mengen/Kosten: aggregated_view nutzen.)r  ru  r   rv  total_cycles_correlatedr  )r  r  r  r  r  )r   r  first_event_daylast_event_daydays_with_dataaggregated_viewtotals
projectionby_program_sizerd  daily_series	anomaliessettings_used)(r   r   r  rm   rp   r   rl   r  r  rC  r   r   r   r   r   r   r   r   r   r  r   r   r  r  r  r  collectionsr  strftimekeysr!  minabsr   r  ro   floatr  r  r   )Mr;   r  r  pump_start_isosize_mapprices_per_washr  r  archiver  r  r   wlr   r  r;  r+  r  r  r   r  matchingr  ru  r  r  prog_size_aggr  r  sizer   r  r  r  variancepricer  prog_only_aggr  r  dailyr  dayr  r   r  recent_daysavg_daily_costr  r  r  avgactualr  	thresholdtotal_archive_mltotal_archive_costtotal_archive_events	first_daylast_daydays_activer  
chem_month	chem_year
chem_todaychem_refr  r  pmmachine_labelr   mkeychem_month_cleanmonthly_costyearly_costr  r  r   )r  r  r  r  r  r  r    compute_archive_aggregations  s  






















$	



	z'SekoPoller.compute_archive_aggregationsr   c                 C   s8  |  ||d< i }tj| jd}tj|rEz$t|dd}t|}W d   n1 s.w   Y  |	dp9i }W n	 t
yD   Y nw | j||d|d< | jd	 }t|d
dd}tj||ddd W d   n1 snw   Y  z	t|| j W n ty   ddl}||| j Y nw t d}|	di }|	di }	|		dd}
|	di }d}|	ddkrd|	dd d|	dd }|	di 	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S )-zSchreibe seko.json (atomar).cycle_correlationr   r   r	   Nwaschmittel_analyse)r  chemical_analysisrD  rE  Fr'   rF  r   z%H:%M:%Sr  r  r  r   r   r}  z | Zyklen korreliert: r  /r  r  r   r   z | Archiv: z (+)[z	] SEKO | r   r   z Pumpen | Monat: r   z.1fz l (r   z.2fu    €) | Jahr: r   z l | Events 24h: )r  r   r   r   r   r   r   r   r   r   r   r  r3   rI  rm   rJ  rK  rL  r   rl   r  rL   )r;   r   analysis_settingsr   r   srN  rK  r  ev_metaev_totalcorr	corr_infor=  r>  archive_infor   r   r    write_output  sv   





zSekoPoller.write_outputr  	days_backc           $      C   s  |   }t|dkrdS td| d g }t|ddD ]}tt  |d d  }|d }t|	d	}d}t
D ]e}	t|	|	dd
 }
||ttt|	dd}z| jjt d|dd}|jdkrhW q=| }W n# ty } ztd| d|	dd  d|  W Y d}~q=d}~ww z|dpi dpi }||	pi }| D ]\}}t|tsq|d|}t| t| d}|dpi }| D ]\}}t|tsq|dd}|r|dd nd}|dpi }| D ]\}}t|tsq|D ]u}t|tsq|dp|dpd}|du s%|s'qt|d}t|d } t|d!}!||tj|d" t j!d#" |	|
||||||t#| d$t#|d%|!t#t|d&d|rpt#| d' | d(ndd) |d7 }qqqqW q= ty } ztd*| d|	dd  d|  W Y d}~q=d}~ww |d+ dks|dkrtd,| d-t| d. t$%d/ q|r| &|\}"}#td0|# d1|" d2 dS td3 dS )4u   Wenn das Archiv leer ist (oder sehr klein), hol einmal die letzten
        N Tage und befülle es. Wird beim Poller-Start aufgerufen.

        Geht in Tages-Chunks vor (Seko-API liefert sonst zu viele Daten auf einmal).
        r  Nz  [Seko] Backfill: hole letzte z Tage in Tages-Chunks...r   rU  r&   iQ r  r   r~   r   r   <   rk   rB   z  [Seko] Backfill-Fehler Tag r      z: r   r   r   r   r   r   r   r   r   rp   r   r   r   r   r   r(   r   r   r   r)   r   z#  [Seko] Backfill Parse-Fehler Tag r\   z  [Seko] Backfill: bei Tag u    · bisher z Events gesammeltg333333?z  [Seko] Backfill fertig: z neue Events ins Archiv (jetzt z insgesamt)z/  [Seko] Backfill ohne Ergebnis (keine Events).)'rC  r!  rL   rangero   r   rl   rp   r  r  r  r  r   rq   rM   rn   r7   rH   rI   rJ   r   r   r   r  r  r   r   r  r  r  r  r   r   r  r  r   timesleepr   )$r;   r)  rZ  all_new_events
day_offsetend_sr   day_strevents_this_dayr{   r   rx   rO   r.  rQ   r/  r0  r1  r2  r3  r   r4  r5  r6  r7  r   r8  r9  r:  r;  r   r   r   r   r   r^  r   r   r    _backfill_archive_if_empty  s   
	
"


-"
z%SekoPoller._backfill_archive_if_emptyc              
   C   s  t d |  st d td |  S z| jdd W n ty6 } zt d|  W Y d}~nd}~ww t dt d	 d
}	 z$|  }d|vrS| 	| d
}n|d7 }|dkret d |   d
}W n ty } zt d|  |d7 }W Y d}~nd}~ww tt qB)zEndlos-Polling.z  [Seko] Starte Poller...z.  [Seko] Login fehlgeschlagen, retry in 60s...r*  r  )r)  z  [Seko] Backfill-Fehler: Nz!  [Seko] Polling gestartet (alle zs)r   Tr   r&   r(   z'  [Seko] 3x fehlgeschlagen, Re-Login...z  [Seko] Fehler: )
rL   rC   r-  r.  
run_pollerr4  r   POLL_INTERVALr?  r(  )r;   rQ   
fail_countr   r   r   r    r5  r  s@   


zSekoPoller.run_pollerr1   )rV   NN)rz   )N)NN)r  )__name__
__module____qualname__r<   r   rC   r  rU   r  ry   strro   r   r?  rC  rO  rA  tupler   r  r  r(  r4  r5  r   r   r   r    r0   h   s6    
1( u@ 3
   B3mr0   c                 C   sB   t | d }t | d d }t | d }|dd|dd|dS )z!Sekunden in HH:MM:SS formatieren.r}   r*  02d:)ro   )secondshr+  r"  r   r   r    r     s   r   r   c              	   C   2   | du r|S zt | W S  ttfy   | Y S w )u   Robuste float-Konvertierung: None, leere Strings, Nicht-Zahlen
    werden zu default statt TypeError. Seko liefert gelegentlich None
    für einzelne Event-Felder (DQ/TQ/FR/DT), das darf nicht crashen.N)r  	TypeError
ValueErrorvaldefaultr   r   r    r    s   
r  c              	   C   rB  )z.Robuste int-Konvertierung (siehe _safe_float).N)ro   rC  rD  rE  r   r   r    r    s   
r  r1   c                 C   sH   t | }|std td dS t| d}tj|jddd}|  |S )uc   Startet den Seko-Poller als Daemon-Thread.
    Wenn keine Credentials vorhanden: gibt None zurück.u@     [Seko] Keine Credentials in settings.json — Poller pausiert.z.  [Seko] Bitte in den Einstellungen eintragen.Nr   Tr0   )targetdaemonr   )r!   rL   r0   	threadingThreadr5  rv   )r   	has_credspollertr   r   r    start_seko_thread  s   
rP  c              
   C   s   t |  t| d}z5| }|sW dS z| }t|pi dg p+t|p&i dg }W n ty8   d}Y nw dd|fW S  tyW } zdt|dfW  Y d}~S d}~ww )	uf   Einmaliger Login-Test für /api/seko/test.
    Gibt (ok: bool, message: str, pumps: int|None) zurück.rH  )Fu0   Login fehlgeschlagen — E-Mail/Passwort prüfenNpumpsr   NT	VerbundenF)r!   r0   rC   r?  r!  r   r   r<  )r   rN  r}  r   rQ  rQ   r   r   r    try_login_with_settings  s"   
,rS  __main__u#   Seko Web Poller — Standalone-Testr'   F)rH  rG  i  )r   )r   r8  )$__doc__r   r   r-  rK  r5   r   r   r   rI   r   r   rM   rn   r6  r!   rq   rr   r  r  r   r   r0   r   r  r  rP  rS  r9  rL   rN  rC   r?  r   r(  dumpsr   r   r   r    <module>   st   (	          7





 