type: custom:stack-in-card variables: lang_override: '' default_tab: 'dist' default_window: 7 chart_height: 280 show_hints: true card_mod: style: | ha-card { border: 3px solid var(--primary-color, #1976D2); border-radius: 16px; background: var(--ha-card-background, #F5FAFE); background: linear-gradient(180deg, color-mix(in srgb, var(--ha-card-background, #F5FAFE) 92%, var(--primary-color, #1976D2)) 0%, color-mix(in srgb, var(--ha-card-background, #F5FAFE) 88%, var(--primary-color, #1976D2)) 50%, color-mix(in srgb, var(--ha-card-background, #F5FAFE) 84%, var(--primary-color, #1976D2)) 100%); box-shadow: 0 10px 28px color-mix(in srgb, var(--primary-color, #1976D2) 20%, transparent); overflow: visible; } cards: - type: custom:mushroom-template-card primary: >- 🎯 DAIKIN CYCLE ML — {{ 'ANALYSE DES CYCLES' if state_attr('sensor.daikin_cycle_ml_cycle_state','configured_language') == 'fr' else 'PATTERN ANALYSIS' }}{{ ' · ' + (state_attr('sensor.daikin_cycle_ml_hp_specs','model')|string|upper) if state_attr('sensor.daikin_cycle_ml_hp_specs','model') else '' }} secondary: >- {% set l = state_attr('sensor.daikin_cycle_ml_cycle_state','configured_language') or 'en' -%} {% if l == 'nl' %}Verdeling · cycles vs duration · COP-efficiëntie{% elif l == 'fr' %}Répartition · cycles / durée · efficacité COP{% else %}Distribution · cycles vs duration · COP efficiency{% endif %} icon: mdi:chart-scatter-plot icon_color: blue multiline_secondary: true card_mod: style: > ha-card { background: linear-gradient(135deg, color-mix(in srgb, var(--ha-card-background, #fff) 85%, var(--primary-color, #1976D2)) 0%, color-mix(in srgb, var(--ha-card-background, #fff) 70%, var(--primary-color, #1976D2)) 100%) !important; box-shadow: none; border-bottom: 3px solid var(--primary-color, #1976D2); border-radius: 0; padding: 12px 16px; } .primary { color: var(--primary-text-color, #0D47A1) !important; font-weight: 900 !important; font-size: 1.05em !important; text-transform: uppercase; letter-spacing: 0.8px; } .secondary { color: var(--primary-color, #1565C0) !important; font-size: 0.88em !important; font-weight: 800 !important; } - type: custom:button-card show_name: false show_icon: false triggers_update: all tap_action: action: javascript javascript: | [[[ try { const card = this; const root = card.shadowRoot || card.getRootNode?.() || card; const chart = root.querySelector('.pa-chart'); if (!chart) return; const openPop = chart.querySelector('.pa-popup'); if (openPop) { const hovered = Array.from(root.querySelectorAll(':hover, :active')); for (const el of hovered) { if (el.closest && el.closest('.pa-popup')) { if (chart.__paTimer) { clearTimeout(chart.__paTimer); chart.__paTimer = null; } openPop.classList.add('pa-popup-out'); setTimeout(() => { if (openPop.parentNode) openPop.parentNode.removeChild(openPop); }, 200); return; } } } let candidates = Array.from(root.querySelectorAll(':hover, :active')).reverse(); if (!candidates.length) candidates = Array.from(root.querySelectorAll('[data-pa-action]')).reverse(); for (const el of candidates) { if (!el || !el.dataset) continue; const action = el.dataset.paAction; if (!action) continue; if (action === 'tab') { const arg = el.dataset.paArg; window.__paTab = arg; chart.setAttribute('data-tab', arg); chart.querySelectorAll('.pa-tab').forEach(t => t.classList.toggle('pa-tab-active', t.dataset.paArg === arg)); return; } if (action === 'win') { const arg = el.dataset.paArg; window.__paWin = arg; chart.setAttribute('data-win', arg); chart.querySelectorAll('.pa-win-btn').forEach(b => b.classList.toggle('pa-active', b.dataset.paArg === arg)); chart.querySelectorAll('.pa-view').forEach(v => v.classList.toggle('pa-view-active', v.dataset.win === arg)); return; } if (action === 'popup') { window.__paPopup(el); return; } if (action === 'close-popup') { const popup = el.closest('.pa-popup') || el; if (popup && popup.parentNode) { if (chart.__paTimer) { clearTimeout(chart.__paTimer); chart.__paTimer = null; } popup.classList.add('pa-popup-out'); setTimeout(() => { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 200); } return; } } } catch(e) { console.warn('[PA]', e); } ]]] hold_action: action: none double_tap_action: action: none styles: card: - background: none - box-shadow: none - border: none - padding: 6px 6px 2px 6px - overflow: visible grid: - grid-template-areas: '"content"' - grid-template-columns: 1fr custom_fields: content: - width: 100% - pointer-events: auto custom_fields: content: | [[[ try { var card = this; card.__paRegistry = card.__paRegistry || {}; card.__paSeq = 0; var CFG = (typeof variables !== 'undefined' && variables) ? variables : {}; var cfgTab = String(CFG.default_tab || 'dist'); var cfgWin = parseInt(CFG.default_window, 10) || 7; var cfgShowHints = CFG.show_hints !== false; var cfgLangOvr = String(CFG.lang_override || '').toLowerCase(); var REGISTRY = { daily_summary: ['daily_summary_recent','recent_dagoverzicht'], hp_specs: ['hp_specs','wp_specs'], cycle_state: ['cycle_state','cyclus_status'], compressor_starts: ['compressor_starts_today','compressorstarts_vandaag'], runtime_buh: ['runtime_buh_today','buh_bedrijfstijd_vandaag'], defrost_count: ['defrost_count_today','ontdooiingen_vandaag'], defrost_duration: ['defrost_duration_today','ontdooiingsduur_vandaag'], duty_cycle: ['duty_cycle_today','duty_cycle_vandaag'], cop_today: ['cop_today','cop_vandaag'], cop_heating_day: ['cop_heating_day','cop_verwarming_dag'], cop_heating_week: ['cop_heating_week','cop_verwarming_week'], cop_heating_month: ['cop_heating_month','cop_verwarming_maand'], cop_dhw_day: ['cop_dhw_day','cop_dhw_dag'], cop_dhw_week: ['cop_dhw_week','cop_dhw_week'], cop_dhw_month: ['cop_dhw_month','cop_dhw_maand'], cop_cooling_day: ['cop_cooling_day','cop_koeling_dag'], cop_cooling_week: ['cop_cooling_week','cop_koeling_week'], cop_cooling_month: ['cop_cooling_month','cop_koeling_maand'], spf_season: ['spf_season','spf_seizoen'], cop_degradation_week: ['cop_degradation_week_pct','cop_degradatie_week_pct'], learned_thresholds: ['learned_thresholds','geleerde_drempels'], cop_curve_recent: ['cop_curve_recent'], cop_vs_datasheet_pct: ['cop_vs_datasheet_pct'] }; var resolveEntity = function(role){ var names = REGISTRY[role]; if(!names) return null; var all = Object.keys(states).filter(function(e){ return e.indexOf('sensor.') === 0; }); var preferred = all.filter(function(e){ return e.indexOf('daikin_cycle_ml') !== -1; }); var i, j; for(i=0; i⚠️ Daikin Cycle ML entities niet gevonden.
Controleer of de integratie draait.
Gezocht: sensor.*_daily_summary_recent / *_recent_dagoverzicht + *cycle_state / *cyclus_status'; } var hasV = function(v){ return v!==null && v!==undefined && v!=='' && String(v)!=='None' && String(v)!=='unknown' && String(v)!=='unavailable'; }; var hasN = function(v){ if (!hasV(v)) return false; var f = parseFloat(v); return isFinite(f); }; var vI = function(v){ if(!hasV(v)||isNaN(parseFloat(v)))return '—'; return Math.round(parseFloat(v)).toString(); }; var attr = function(id,n,d){ try{ if(!id)return d; var a=states[id] && states[id].attributes; if(!a)return d; var v=a[n]; if(v===undefined||v===null||String(v)==='None')return d; return v; } catch(e){ return d; } }; var getState = function(eid){ try { var st = states[eid]; if (!st) return null; var v = parseFloat(st.state); return isFinite(v) ? v : null; } catch(e){ return null; } }; var weekday = function(iso){ if(!iso) return ''; try{ var d = new Date(iso + 'T12:00:00'); return (LANG==='fr'?['dim','lun','mar','mer','jeu','ven','sam']:['zo','ma','di','wo','do','vr','za'])[d.getDay()]; }catch(e){ return ''; } }; var fmtDay = function(iso){ if(!iso) return '—'; var p=String(iso).split('-'); return p.length>=3 ? p[2]+'/'+p[1] : iso; }; var fmtMin = function(m){ if(!hasV(m)||!isFinite(Number(m))) return '—'; m = Number(m); if(m < 60) return Math.round(m)+'m'; var h = Math.floor(m/60); var mm = Math.round(m - h*60); return mm === 0 ? h+'u' : h+'u'+String(mm).padStart(2,'0'); }; var fmt1 = function(v){ return hasN(v) ? Number(v).toFixed(1).replace('.',',') : '—'; }; var pct = function(v){ return hasN(v) ? (Number(v)>0?'+':'') + Number(v).toFixed(1).replace('.',',') + '%' : '—'; }; var arrow = function(v){ return !hasN(v) ? '' : (Number(v)>0?'▲':Number(v)<0?'▼':'●'); }; /* THEME */ var DARK = !!(hass && hass.themes && hass.themes.darkMode); var TH = { dark: DARK, bg: DARK ? 'var(--card-background-color, #1C1C1E)' : 'var(--card-background-color, #FFFFFF)', bg2: DARK ? 'var(--secondary-background-color, #2C2C2E)' : 'var(--secondary-background-color, #F5FAFE)', text: DARK ? 'var(--primary-text-color, #E5E5EA)' : 'var(--primary-text-color, #0D47A1)', sub: DARK ? 'var(--secondary-text-color, #AEAEB2)' : 'var(--secondary-text-color, #546E7A)', accent: DARK ? 'var(--primary-color, #64B5F6)' : 'var(--primary-color, #1976D2)', accent2: DARK ? '#90CAF9' : '#0D47A1', border: DARK ? 'rgba(255,255,255,0.14)' : 'rgba(13,71,161,0.12)', grid: DARK ? 'rgba(255,255,255,0.07)' : 'rgba(13,71,161,0.08)', green: DARK ? '#66BB6A' : '#2E7D32', orange: DARK ? '#FFA726' : '#E65100', red: DARK ? '#EF5350' : '#C62828', blue: DARK ? '#42A5F5' : '#1976D2', purple: DARK ? '#BA68C8' : '#6A1B9A', buh: DARK ? '#FFA726' : '#E65100', defrost: DARK ? '#42A5F5' : '#1976D2' }; var TXT_HALO = DARK ? 'paint-order="stroke fill" stroke="#000" stroke-width="2.5" stroke-opacity="0.65" stroke-linejoin="round"' : 'paint-order="stroke fill" stroke="white" stroke-width="2.5" stroke-opacity="0.9" stroke-linejoin="round"'; var qColor = function(q){ return (q===null||q===undefined) ? TH.sub : (q>=70?TH.green:q>=40?TH.orange:TH.red); }; var durCol = function(m){ return m<20?TH.red:m<60?TH.orange:m<180?TH.green:TH.blue; }; var MODE_COL = { heating: TH.red, dhw: TH.blue, cooling: '#42A5F5' }; var MODE_ICON = { heating: '🔥', dhw: '🚿', cooling: '🧊' }; var T = function(x, y, text, o){ o = o || {}; return ''+text+''; }; if (!window.__paRegistry) window.__paRegistry = {}; if (!window.__paRegSeq) window.__paRegSeq = 0; if (window.__paRegSeq > 900) { window.__paRegistry = {}; window.__paRegSeq = 0; } var popAttr = function(payload){ window.__paRegSeq++; var id = 'p' + window.__paRegSeq; window.__paRegistry[id] = payload; return 'data-pa-action="popup" data-pa-pid="'+id+'"'; }; if (!window.__paPopup) { window.__paPopup = function(el){ try { var chart = el.closest('.pa-chart'); if(!chart) return; var host = chart.querySelector('.pa-popup-host'); if(!host) return; var pid = el.getAttribute('data-pa-pid'); if(!pid) return; var payload = window.__paRegistry[pid]; if(!payload) return; var now = Date.now(); if (chart.__paLastTap && (now - chart.__paLastTap) < 180) return; chart.__paLastTap = now; var oldPops = host.querySelectorAll('.pa-popup'); for (var i=0; i✕'; if (payload.items) for (var j=0; j'+it.v+''; continue; } html += '
'+it.k+''+it.v+'
'; } if (payload.modes && payload.modes.length) { html += '
'; for (var mi=0; mi'+md.cycles+'×'+md.dur+'Q'+md.q+'
'; } html += ''; } html += '
'; pop.innerHTML = html; pop.style.visibility='hidden'; pop.style.left='0px'; pop.style.top='0px'; host.appendChild(pop); var chartRect = chart.getBoundingClientRect(); var rect; try { rect = el.getBoundingClientRect(); } catch(e){ rect = null; } var pw = pop.offsetWidth || 250, ph = pop.offsetHeight || 200; var ex=0, ey=40, ew=100, eh=20; if (rect && rect.width > 0) { ex = rect.left - chartRect.left; ey = rect.top - chartRect.top; ew = rect.width; eh = rect.height; } else { ex = chartRect.width/2 - 50; } var GAP = 12, MARGIN = 6; var left = ex + ew/2 - pw/2; if (left < MARGIN) left = MARGIN; if (left + pw > chartRect.width - MARGIN) left = chartRect.width - pw - MARGIN; var placeAbove = (ey - GAP - ph) >= MARGIN; var top; if (placeAbove) { top = ey - GAP; pop.classList.add('popup-above'); } else { var belowTop = ey + eh + GAP; top = (belowTop+ph <= chartRect.height-MARGIN) ? belowTop : (MARGIN+4); pop.classList.add('popup-below'); } pop.style.left = left+'px'; pop.style.top = top+'px'; pop.style.visibility='visible'; pop.addEventListener('click', function(ev){ ev.stopImmediatePropagation(); ev.stopPropagation(); ev.preventDefault(); if (chart.__paTimer) { clearTimeout(chart.__paTimer); chart.__paTimer = null; } pop.classList.add('pa-popup-out'); setTimeout(function(){ if (pop.parentNode) pop.parentNode.removeChild(pop); }, 200); }, true); chart.__paTimer = setTimeout(function(){ if (pop && pop.parentNode) { pop.classList.add('pa-popup-out'); setTimeout(function(){ if (pop.parentNode) pop.parentNode.removeChild(pop); }, 250); } chart.__paTimer = null; }, 5000); } catch(e){ console.warn('[PA popup]', e); } }; } var cfgLang = String(attr(STATE,'configured_language','en')).toLowerCase(); var haLang = (typeof hass !== 'undefined' && hass && hass.language) ? String(hass.language).toLowerCase() : ''; var LANG = 'en'; if (cfgLangOvr === 'nl' || cfgLangOvr === 'en' || cfgLangOvr === 'fr') { LANG = cfgLangOvr; } else if (haLang.indexOf('nl') === 0) { LANG = 'nl'; } else if (cfgLang === 'nl') { LANG = 'nl'; } else if (cfgLang === 'fr' || haLang.indexOf('fr') === 0) { LANG = 'fr'; } /* FR-I18N (azsono03) */ var FRM={" cycles in IDEAL zone":" cycles dans la zone IDÉALE"," cycles in PENDULUM zone":" cycles dans la zone TROP COURTE"," cycles over ":" cycles sur ","24h vs 7d":"24 h / 7 j","30d":"30 j","365d":"365 j","Last 7d":"7 derniers jours","SHORT 20-60m":"COURT 20-60 min","GOOD 60-180m":"BON 60-180 min","LONG >180m":"LONG >180 min","40-70 mid":"40-70 moyen","<40 low":"<40 faible","ACTION REQUIRED":"ACTION REQUISE","ATTENTION":"ATTENTION","AVG DURATION":"DURÉE MOY.","Average quality ":"Qualité moyenne ","Average quality low (":"Qualité moyenne faible (","Avg duration":"Durée moy.","Avg off":"Arrêt moy.","Avg quality":"Qualité moy.","Avg ratio":"Taux moy.","Avg/day":"Moy./jour","BUH":"Appoint","BUH >":"Appoint >","Band":"Plage","Best day":"Meilleur jour","Boiler COP":"COP chaudière","Bucket":"Tranche","CARNOT EFF.":"RENDEMENT CARNOT","COMPRESSOR":"COMPRESSEUR","COOLING":"RAFRAÎCHISSEMENT","COP":"COP","COP TODAY":"COP DU JOUR","COP TREND":"TENDANCE COP","COP above datasheet reference":"COP au-dessus de la fiche technique","CYCLES PER DAY":"CYCLES PAR JOUR","CYCLES/DAY":"CYCLES/JOUR","Comfort":"Confort","Compressor off":"Compresseur arrêté","Contribution":"Part","Cool":"Froid","Cooling":"Rafraîchissement","Cycles":"Cycles","Cycles per day":"Cycles par jour","DEFROST":"DÉGIVRAGE","DEFROST IMPACT":"EFFET DES DÉGIVRAGES","DHW":"ECS","DURATION/DAY":"DURÉE/JOUR","Days":"Jours","Defrost":"Dégivrage","Duration":"Durée","Duration per day":"Durée par jour","Duty":"Taux de marche","ELEC":"ÉLEC","ELECTRICAL":"ÉLECTRIQUE","EMITTER":"RADIATEURS","ENERGY TODAY":"ÉNERGIE DU JOUR","Elapsed":"Écoulé","Energy":"Énergie","Event counts per day":"Événements par jour","Event ratio per day":"Taux d'événements par jour","Good days":"Bons jours","Good ratio":"Taux de bons cycles","HEAT BALANCE":"BILAN DE CHALEUR","HEAT PUMP":"PAC","HEATING":"CHAUFFAGE","HEATING OFF":"CHAUFFAGE COUPÉ","HP → EMITTER":"PAC → RADIATEURS","Heat":"Chauffage","Heating":"Chauffage","IDLE":"À L'ARRÊT","IN":"ENTRÉE","INFO":"INFO","Insufficient data":"Pas assez de données","LWT":"Départ","LWT NOW":"DÉPART ACTUEL","LWT OPT":"DÉPART OPTIMAL","Last defrost":"Dernier dégivrage","Last: ":"Dernier : ","Live cycle · COP · quality · alerts":"Cycle en direct · COP · qualité · alertes","Longest":"Le plus long","Longest day":"Jour le plus long","Low days":"Jours faibles","MAX LWT":"DÉPART MAX","MIN LWT":"DÉPART MIN","MODES":"MODES","Max day":"Jour max","Mid days":"Jours moyens","Mode":"Mode","Month":"Mois","No COP data":"Pas de COP","No data":"Pas de données","OUT":"SORTIE","OUTDOOR TEMPERATURE (°C)":"TEMPÉRATURE EXTÉRIEURE (°C)","Outdoor":"Extérieur","PENDULUM <20m":"TROP COURT <20 min","Phase":"Phase","Power":"Puissance","QUALITY":"QUALITÉ","Quality":"Qualité","Quality dropping (":"Qualité en baisse (","Quality per day":"Qualité par jour","Quality rising (+":"Qualité en hausse (+","RPS":"tr/s","RUNNING":"EN MARCHE","Range":"Plage","Reliability":"Fiabilité","Runs":"Cycles","Runtime":"Durée de marche","SAVINGS":"ÉCONOMIE","SPF SEASON":"SPF SAISON","STANDBY":"EN ATTENTE","SUPPLY LWT (°C)":"DÉPART D'EAU (°C)","Samples":"Échantillons","Savings":"Économie","Share":"Part","Short runs":"Cycles courts","Since midnight":"Depuis minuit","Source":"Source","Span":"Période","Starts":"Démarrages","Starts today":"Démarrages du jour","State":"État","Step":"Pas","Sum":"Somme","System performing normally":"Le système fonctionne normalement","THERM":"CHALEUR","THERMAL":"CHALEUR","THERMAL SPLIT":"RÉPARTITION DE LA CHALEUR","Today":"Aujourd'hui","Total":"Total","Total BUH":"Appoint total","Total defrost":"Dégivrage total","Total runtime":"Marche totale","Trends":"Tendances","VS DATASHEET":"VS FICHE TECHNIQUE","WEEK TREND":"TENDANCE SEMAINE","Week":"Semaine","Worst day":"Pire jour","Y-axis max":"Échelle max","Year":"Année","act":"act.","active":"active","ago":"plus tôt","avg":"moy.","buckets":"tranches","cycles":"cycles","datasheet":"fiche technique","day":"jour","defrost <":"dégivrage <","demand":"demande","duration":"durée","events":"événements","heating OFF >":"chauffage coupé >","high":"élevé","in bucket":"dans la tranche","insufficient buckets":"pas assez de tranches","last":"dernier","low":"faible","max":"max","min":"min","month":"mois","more":"de plus","nominal":"normal","nominal COP":"COP nominal","of cycles":"des cycles","outdoor":"extérieur","per day":"par jour","samples":"échantillons","state":"état","supply":"départ","today":"aujourd'hui","total":"total","week":"semaine","Ø duration":"Ø durée","Ø/cycle":"Ø/cycle","← 💧 RETURN":"← 💧 RETOUR","≥70 good":"≥70 bon","⏮️ Last cycle":"⏮️ Dernier cycle","⏱️ DURATION":"⏱️ DURÉE","♻️ Hydraulic circuit":"♻️ Circuit hydraulique","⚠ BUH ASSIST":"⚠ APPOINT","⚡ BUH":"⚡ Appoint","⚡ EVENTS":"⚡ ÉVÉNEMENTS","✅ System OK — no active warnings":"✅ Système OK — aucune alerte active","❄ DEFROST":"❄ DÉGIVRAGE","❄ Defrost":"❄ Dégivrage","⭐ QUALITY":"⭐ QUALITÉ","🎯 COP":"🎯 COP","🎯 COP Efficiency":"🎯 Efficacité COP","🎯 Cycles vs Duration":"🎯 Cycles / durée","🎯 SCATTER":"🎯 NUAGE","💡 COP = efficiency · ~ref = approximate datasheet comparison (A7/W35) · duration/cycle trends per day":"💡 COP = efficacité · ~réf = comparaison approximative avec la fiche (A7/W35) · tendances durée/cycles par jour","💡 Cycle = continuous compressor run · <15m = inefficient cycling · >48h = safety limit":"💡 Cycle = marche continue du compresseur · <15 min = cycles trop courts · >48 h = limite de sécurité","💡 Duration = total compressor runtime per day · split per mode · ⚠ = clipped":"💡 Durée = marche totale du compresseur par jour · par mode · ⚠ = tronqué","💡 Higher COP = better · Q = quality 0-100 · Alerts on top = action":"💡 COP élevé = mieux · Q = qualité 0-100 · Alertes en haut = à traiter","💡 Ratio = events per cycle × 100 · green = nominal (15%) · red = elevated (35%)":"💡 Taux = événements par cycle × 100 · vert = normal (15 %) · rouge = élevé (35 %)","💡 Tap a bar for details — buckets by avg cycle duration per mode":"💡 Toucher une barre pour le détail — tranches par durée moyenne de cycle et par mode","💡 Tap a dot for details — X: cycles/day · Y: avg duration · color: quality":"💡 Toucher un point pour le détail — X : cycles/jour · Y : durée moy. · couleur : qualité","💡 Tap a dot or stat for day details":"💡 Toucher un point ou une valeur pour le détail du jour","💡 Tap an item for details":"💡 Toucher un élément pour le détail","📅 Today":"📅 Aujourd'hui","📈 Heating curve advice":"📈 Conseil de loi d'eau","📈 Regression":"📈 Régression","📊 Cycle duration distribution":"📊 Répartition des durées de cycle","📊 DISTRIBUTION":"📊 RÉPARTITION","📊 OVERVIEW":"📊 VUE D'ENSEMBLE","📊 historical":"📊 historique","🔄 CYCLES":"🔄 CYCLES","🔍 auto-discovery":"🔍 détection auto","🔥 SUPPLY →":"🔥 DÉPART →"," below datasheet":" sous la fiche technique"," well above target":" bien au-dessus de l'objectif","% week":" % / semaine","BUH active":"Appoint en marche","COP dropping ":"COP en baisse ","COP heating ":"COP chauffage ","COP low":"COP bas","DHW cycling":"Cycles d'eau chaude","High cycle rate":"Fréquence de cycles élevée","Missing attributes":"Valeurs manquantes","Pendulum/day":"Cycles trop fréquents/jour","Pendulum/hour":"Cycles trop fréquents/heure","Previous: BUH":"Précédent : appoint","Previous: defrost":"Précédent : dégivrage","SPF ":"SPF ","Setpoint oscillation":"Consigne instable","Short off-time":"Arrêt court","Short run":"Marche courte","Source stale":"Source muette","Too many cycles (target ":"Trop de cycles (objectif ","Update available":"Mise à jour disponible","is_stale flag":"Source en retard"}; var FR=function(x){ return (LANG==='fr' && typeof x==='string' && Object.prototype.hasOwnProperty.call(FRM,x)) ? FRM[x] : x; }; var FRO=function(o){ if(typeof o==='string') return Object.prototype.hasOwnProperty.call(FRM,o)?FRM[o]:o; if(o&&typeof o==='object'){ var r=Array.isArray(o)?[]:{}; for(var k in o){ r[k]=FRO(o[k]); } return r; } return o; }; var I18N = { hdr_dist: {nl:'📊 Verdeling cyclus-duur', en:'📊 Cycle duration distribution'}, hdr_scatter: {nl:'🎯 Cycles vs Duur', en:'🎯 Cycles vs Duration'}, hdr_cop: {nl:'🎯 COP Efficiëntie', en:'🎯 COP Efficiency'}, cnt_perday: {nl:'per dag', en:'per day'}, tab_dist: {nl:'📊 VERDELING', en:'📊 DISTRIBUTION'}, tab_scatter: {nl:'🎯 SCATTER', en:'🎯 SCATTER'}, tab_cop: {nl:'🎯 COP', en:'🎯 COP'}, axis_x_cyc: {nl:'CYCLES PER DAG', en:'CYCLES PER DAY'}, axis_y_dur: {nl:'GEM. DUUR', en:'AVG DURATION'}, r_cycles: {nl:'Cycli', en:'Cycles'}, r_avgdur: {nl:'Ø duur', en:'Ø duration'}, r_maxdur: {nl:'Langste', en:'Longest'}, r_totaldur: {nl:'Totale duur', en:'Total runtime'}, r_quality: {nl:'Kwaliteit', en:'Quality'}, r_buh: {nl:'⚡ BUH', en:'⚡ BUH'}, r_defrost: {nl:'❄ Defrost', en:'❄ Defrost'}, r_days: {nl:'Dagen', en:'Days'}, r_share: {nl:'Aandeel', en:'Share'}, r_samples: {nl:'Samples', en:'Samples'}, note_dist: {nl:'💡 Tik op een balk voor details — buckets op basis van gemiddelde cyclusduur per modus', en:'💡 Tap a bar for details — buckets by avg cycle duration per mode'}, note_scatter: {nl:'💡 Tik op een punt voor details — X: cycli/dag · Y: gem. duur · kleur: kwaliteit', en:'💡 Tap a dot for details — X: cycles/day · Y: avg duration · color: quality'}, note_cop: {nl:'💡 COP = efficiëntie · ~ref = benaderde datasheet-vergelijking (A7/W35) · duur/cycli-trends per dag', en:'💡 COP = efficiency · ~ref = approximate datasheet comparison (A7/W35) · duration/cycle trends per day'}, cop_today: {nl:'COP VANDAAG', en:'COP TODAY'}, cop_vs_ds: {nl:'VS DATASHEET', en:'VS DATASHEET'}, spf_season: {nl:'SPF SEIZOEN', en:'SPF SEASON'}, week_trend: {nl:'WEEKTREND', en:'WEEK TREND'}, buh_impact: {nl:'⚡ BUH', en:'⚡ BUH'}, defrost_n: {nl:'❄ DEFROST', en:'❄ DEFROST'}, no_cop_data: {nl:'Geen COP-data', en:'No COP data'} }; var t = function(k){ var e=I18N[k]; if(!e) return k; if(LANG==='fr') return e.fr || FR(e.en || e.nl); return e[LANG] || e.en || e.nl; }; var rawDays = attr(SUMMARY, 'days', []); if(!Array.isArray(rawDays)) rawDays = []; var rdDurMin = function(rd){ return hasN(rd.dur_min) ? parseFloat(rd.dur_min) : (hasN(rd.total_duration_s) ? parseFloat(rd.total_duration_s)/60 : 0); }; var rdDurMax = function(rd){ return hasN(rd.dur_max) ? parseFloat(rd.dur_max) : (hasN(rd.duration_max) ? parseFloat(rd.duration_max)/60 : 0); }; var rdDurAvg = function(rd){ if (hasN(rd.dur_min_avg)) return parseFloat(rd.dur_min_avg); if (hasN(rd.duration_avg)) return parseFloat(rd.duration_avg)/60; if (hasN(rd.dur_min) && rd.cycles > 0) return parseFloat(rd.dur_min)/rd.cycles; if (hasN(rd.total_duration_s) && rd.cycles > 0) return parseFloat(rd.total_duration_s)/60/rd.cycles; return 0; }; var rdBuh = function(rd){ return hasN(rd.buh) ? parseFloat(rd.buh) : (hasN(rd.buh_count) ? parseFloat(rd.buh_count) : 0); }; var rdDefrost = function(rd){ return hasN(rd.defrost) ? parseFloat(rd.defrost) : (hasN(rd.defrost_count) ? parseFloat(rd.defrost_count) : 0); }; var byDate = {}; for(var ri=0; ri e.dur_max_min) e.dur_max_min = dMax; e.buh += buhV; e.defrost += defrV; e.transient += (rd.transient_count||0); if (qaV !== null) { e.quality_sum += qaV*Math.max(1,cycN); e.quality_weight += Math.max(1,cycN); } var mkey = rd.mode || 'unknown'; if(e.modes[mkey]){ var mm = e.modes[mkey]; mm.cycles += cycN; mm.dur_total_min += dMin; if (dMax > mm.dur_max_min) mm.dur_max_min = dMax; if (dtV !== null) mm.dt = dtV; if (rpsV !== null) mm.rps = rpsV; if (qaV !== null) mm.quality = qaV; mm.buh += buhV; mm.defrost += defrV; } } var daysAll = Object.keys(byDate).map(function(k){ var e = byDate[k]; e.quality_avg = e.quality_weight > 0 ? e.quality_sum / e.quality_weight : null; e.dur_avg = e.cycles > 0 ? e.dur_total_min / e.cycles : 0; Object.keys(e.modes).forEach(function(mk){ var mm = e.modes[mk]; mm.avg_min = mm.cycles > 0 ? mm.dur_total_min / mm.cycles : 0; mm.total_min = mm.dur_total_min; mm.dur = mm.total_min; mm.avg = mm.avg_min; }); return e; }).sort(function(a,b){ return a.day < b.day ? -1 : 1; }); var activeWin = String(window.__paWin || cfgWin); var activeTab = String(window.__paTab || cfgTab); var buildModeChips = function(entry){ var arr = []; ['heating','dhw','cooling'].forEach(function(mk){ var mm = entry.modes[mk]; if (!mm || mm.cycles <= 0) return; arr.push({icon:MODE_ICON[mk], cycles:mm.cycles, dur:fmtMin(mm.total_min), q:mm.quality===null?'—':vI(mm.quality), qc:qColor(mm.quality)}); }); return arr; }; /* DIST */ var buildDistContent = function(sub, wkey){ var windowSet = {}; sub.forEach(function(d){ windowSet[d.day]=true; }); var dist = [ {label:(LANG==='fr'?'TROP COURT':'PENDEL'), sub:'<20m', col:TH.red, bg:DARK?'rgba(239,83,80,0.10)':'#FFEBEE', n:0, durSum:0, days:{}}, {label:(LANG==='fr'?'COURT':'KORT'), sub:'20-60m', col:TH.orange, bg:DARK?'rgba(255,167,38,0.10)':'#FFF3E0', n:0, durSum:0, days:{}}, {label:(LANG==='fr'?'BON':'GOED'), sub:'60-180m', col:TH.green, bg:DARK?'rgba(102,187,106,0.10)':'#E8F5E9', n:0, durSum:0, days:{}}, {label:(LANG==='fr'?'LONG':'LANG'), sub:'>180m', col:TH.blue, bg:DARK?'rgba(66,165,245,0.10)':'#E3F2FD', n:0, durSum:0, days:{}} ]; rawDays.forEach(function(rd){ if(!rd.day || !windowSet[rd.day] || !rd.cycles || rd.cycles <= 0) return; var avg = rdDurAvg(rd); if(!isFinite(avg) || avg <= 0) return; var idx = avg<20?0:avg<60?1:avg<180?2:3; dist[idx].n += rd.cycles; dist[idx].durSum += rdDurMin(rd); dist[idx].days[rd.day] = (dist[idx].days[rd.day]||0) + rd.cycles; }); var totCycles = dist.reduce(function(a,o){ return a+o.n; }, 0); var distMax = Math.max(1, dist[0].n, dist[1].n, dist[2].n, dist[3].n); var domIdx = 0, domN = -1; for(var i=0; i<4; i++){ if(dist[i].n > domN){ domN = dist[i].n; domIdx = i; } } var dW=460, dH=260, dPadL=42, dPadR=12, dPadT=22, dPadB=100; var dPlotW = dW-dPadL-dPadR, dPlotH = dH-dPadT-dPadB; var dSlotW = dPlotW/4, dBarW = dSlotW*0.62; var dY = function(v){ return dPadT + dPlotH - (v/distMax)*dPlotH; }; var gid = function(n){ return n+'_'+wkey; }; var s = ''; ['red','org','grn','blu'].forEach(function(k, idx){ var c = [TH.red,TH.orange,TH.green,TH.blue][idx]; s += ''; }); s += ''; s += ''; for(var yi=0; yi<=4; yi++){ var yv = Math.round(distMax*yi/4), yy = dY(yv); s += ''; s += T(dPadL-6, yy+4, yv, {a:'end'}); } for(var di=0; di<4; di++){ var o = dist[di]; var cx = dPadL + di*dSlotW + dSlotW/2; var bH = (o.n/distMax)*dPlotH; var byTop = dPadT + dPlotH - bH; var isDom = (di === domIdx && o.n > 0); var dayList = Object.keys(o.days).sort(); var sharePct = totCycles > 0 ? Math.round(o.n/totCycles*100) : 0; var items = [ {t:'hl', k:t('r_cycles'), v:String(o.n), c:o.col}, {t:'sep'}, {t:'kv', k:t('r_days'), v:String(dayList.length), c:TH.sub}, {t:'kv', k:t('r_share'), v:sharePct+'%', c:o.col}, {t:'kv', k:t('r_avgdur'), v:(o.n>0?fmtMin(Math.round(o.durSum/o.n)):'—'), c:o.col}, {t:'kv', k:t('r_totaldur'), v:(o.n>0?fmtMin(Math.round(o.durSum)):'—'), c:TH.text} ]; if (dayList.length > 0) { items.push({t:'sep'}); for (var dii=0; dii'; if(o.n > 0){ var gradRef = 'url(#'+gid('pg-'+(di===0?'red':di===1?'org':di===2?'grn':'blu'))+')'; var filt = isDom ? ' filter="url(#'+gid('pg-glow')+')"' : ''; s += ''; s += ''; s += ''; s += ''; s += T(cx, byTop-9, o.n, {a:'middle', s:18, w:900, f:o.col, halo:1}); if(isDom) s += '👑'; } else { s += T(cx, dPadT+dPlotH/2+6, '0', {a:'middle', s:14, w:900, f:o.col, attr:'opacity="0.3"', halo:1}); } var labelY = dPadT + dPlotH + 18; s += T(cx, labelY, o.label, {a:'middle', s:11, w:900, f:o.col, attr:'letter-spacing="0.5"', halo:1}); s += T(cx, labelY+14, o.sub, {a:'middle', s:9, w:800, f:TH.sub, halo:1}); s += T(cx, labelY+30, Math.round(o.n/Math.max(1,totCycles)*100)+'%', {a:'middle', s:10, w:900, f:o.col, halo:1}); if(o.n>0) s += T(cx, labelY+44, 'Ø '+fmtMin(Math.round(o.durSum/o.n)), {a:'middle', s:9.5, w:900, f:o.col, halo:1}); } s += ''; s += ''; return s; }; /* SCATTER */ var buildScatterContent = function(sub, wkey){ var scW=460, scH=350, scPadL=58, scPadR=16, scPadT=26, scPadB=54; var scPlotW = scW-scPadL-scPadR, scPlotH = scH-scPadT-scPadB; var scXMax = Math.max(6, Math.max.apply(null, sub.map(function(d){ return d.cycles; })) + 1); var scYMaxRaw = Math.max.apply(null, sub.map(function(d){ return d.dur_avg; })); var scYMax = Math.max(60, Math.ceil(scYMaxRaw/60)*60); var scX = function(v){ return scPadL + (v/scXMax)*scPlotW; }; var scY = function(v){ return scPadT + scPlotH - (v/scYMax)*scPlotH; }; var xHalf = Math.min(scXMax-0.5, 5); var gid = function(n){ return n+'_'+wkey; }; var subTotalCycles = sub.reduce(function(a,d){ return a+d.cycles; }, 0); var pts = []; for(var si=0; si'; s += ''; s += ''; s += ''; s += ''; s += ''; var qx = scX(xHalf), qyG = scY(60), qyS = scY(20); s += ''; s += ''; s += ''; s += ''; for(var gy=0; gy<=4; gy++){ var gyv = scYMax*gy/4, gyy = scY(gyv); s += ''; s += T(scPadL-8, gyy+4, Math.round(gyv)+'m', {a:'end'}); } for(var gx=0; gx<=4; gx++){ var gxv = scXMax*gx/4; s += T(scX(gxv), scPadT+scPlotH+18, Math.round(gxv), {a:'middle'}); } s += ''; s += ''; for(var pj=0; pj0 ? Math.round(dS.cycles/subTotalCycles*100) : 0; var items = [ {t:'hl', k:t('r_quality'), v:(dS.quality_avg===null?'—':vI(dS.quality_avg)+'%'), c:sc2}, {t:'sep'}, {t:'kv', k:t('r_cycles'), v:String(dS.cycles), c:TH.text}, {t:'kv', k:t('r_share'), v:sharePct+'%', c:TH.sub}, {t:'kv', k:t('r_avgdur'), v:fmtMin(dS.dur_avg), c:TH.text}, {t:'kv', k:t('r_maxdur'), v:fmtMin(dS.dur_max_min), c:TH.text}, {t:'kv', k:t('r_totaldur'), v:fmtMin(dS.dur_total_min), c:TH.text}, {t:'sep'}, {t:'kv', k:t('r_buh'), v:String(dS.buh||0), c:(dS.buh>0?TH.buh:TH.sub)}, {t:'kv', k:t('r_defrost'), v:String(dS.defrost||0), c:(dS.defrost>0?TH.defrost:TH.sub)} ]; var pattr = popAttr({title: weekday(dS.day)+' '+fmtDay(dS.day), items: items, modes: buildModeChips(dS)}); s += ''; s += ''; if (p.isLatest || p.isToday){ s += ''; } s += ''; s += ''; s += ''; var lblY = tryPlace(p.x, p.y, p.r); if (lblY !== null){ s += T(p.x, lblY, weekday(dS.day), {a:'middle', s:p.isLatest?10:9, w:900, f:TH.text, halo:1}); var durLblY = lblY < p.y ? lblY - 12 : lblY + 12; s += T(p.x, durLblY, fmtMin(dS.dur_avg), {a:'middle', s:p.isLatest?8:7, w:800, f:TH.sub, halo:1}); } } s += ''; s += ''; s += T(scPadL+scPlotW/2, scH-8, t('axis_x_cyc'), {a:'middle', s:11, w:900, attr:'letter-spacing="0.5"'}); s += T(10, scPadT+scPlotH/2, t('axis_y_dur'), {a:'middle', s:11, w:900, attr:'transform="rotate(-90 10 '+(scPadT+scPlotH/2)+')"'}); return s; }; /* COP */ var buildCopContent = function(sub, wkey){ var W = 460, H = 460, P = 12; var CW = (W - P*3) / 2; var gid = function(n){ return n+'_'+wkey; }; var secTitle = function(y, txt) { return T(P, y, txt, {s:9, w:900, f:TH.sub, attr:'letter-spacing="0.5"'}); }; var modeData = {}; ['heating','dhw','cooling'].forEach(function(mk){ var day = getState(resolveEntity('cop_'+mk+'_day')); var week = getState(resolveEntity('cop_'+mk+'_week')); var month = getState(resolveEntity('cop_'+mk+'_month')); var dayAttr = states[resolveEntity('cop_'+mk+'_day')] ? states[resolveEntity('cop_'+mk+'_day')].attributes : null; var stats = (dayAttr && dayAttr.by_mode && dayAttr.by_mode[mk]) ? dayAttr.by_mode[mk] : null; modeData[mk] = {day:day, week:week, month:month, stats:stats}; }); var copComb = getState(resolveEntity('cop_today')); var copTodayAttrs = states[resolveEntity('cop_today')] ? states[resolveEntity('cop_today')].attributes : null; var copToday = getState(resolveEntity('cop_today')); if (copToday === null && copComb !== null) copToday = copComb; var copMin = (copTodayAttrs && hasN(copTodayAttrs.cop_min)) ? parseFloat(copTodayAttrs.cop_min) : null; var copMax = (copTodayAttrs && hasN(copTodayAttrs.cop_max)) ? parseFloat(copTodayAttrs.cop_max) : null; var copSamples = (copTodayAttrs && hasN(copTodayAttrs.samples_today)) ? parseInt(copTodayAttrs.samples_today) : null; var spf = getState(resolveEntity('spf_season')); var degr = getState(resolveEntity('cop_degradation_week')); var buhRt = getState(resolveEntity('runtime_buh')); var defrCnt = getState(resolveEntity('defrost_count')); var dsSensor = getState(resolveEntity('cop_vs_datasheet_pct')); var hpSpecs = states[HP_SPECS] ? states[HP_SPECS].attributes : null; var dsRef = null; if (hpSpecs && Array.isArray(hpSpecs.points)) { for (var pi=0; pi 0) tmp.push({cop: cv, mode: cp.mode, ts: cp.ts}); } if (tmp.length >= 2) curve = tmp; } var s = ''; s += secTitle(10, 'COP PERFORMANCE'); s += secTitle(110, 'MODE BREAKDOWN'); s += secTitle(232, 'DURATION & CYCLES TREND'); s += secTitle(364, 'SYSTEM HEALTH'); /* ROW 1: Header */ var copHeaderItems = [ {t:'hl', k:'COP', v:fmt1(copToday), c:TH.accent2}, {t:'sep'} ]; if (copMin !== null) copHeaderItems.push({t:'kv', k:'Min', v:fmt1(copMin), c:TH.red}); if (copMax !== null) copHeaderItems.push({t:'kv', k:'Max', v:fmt1(copMax), c:TH.green}); if (copSamples !== null) copHeaderItems.push({t:'kv', k:t('r_samples'), v:String(copSamples), c:TH.sub}); if (curve) copHeaderItems.push({t:'kv', k:'48h', v:curve.length+'h', c:TH.sub}); var headerPop = popAttr({title: t('cop_today'), items: copHeaderItems}); s += ''; s += T(P, 26, t('cop_today'), {s:9, w:900, f:TH.sub}); s += T(P, 60, fmt1(copToday), {s:32, w:900, f:TH.accent2, halo:1}); s += ''; s += ''; if (copSamples !== null) { s += T(P+72, 60, copSamples+'s', {s:8, w:800, f:TH.sub}); } if (curve && curve.length >= 2) { var spkX = P+108, spkY = 36, spkW = 100, spkH = 20; var spkCops = curve.map(function(c){ return c.cop; }); var spkMin = Math.min.apply(null, spkCops); var spkMax = Math.max.apply(null, spkCops); var spkRng = Math.max(0.1, spkMax - spkMin); var spkPath = ''; for (var spi=0; spi'; s += ''; var lastY = spkY + spkH - ((curve[curve.length-1].cop - spkMin)/spkRng)*spkH; s += ''; s += ''; s += ''; } if (copMin !== null && copMax !== null && copMax > copMin) { var bandX = P+108, bandY = 60, bandW = 100, bandH = 4; var bandPos = bandX + ((copToday - copMin)/(copMax-copMin))*bandW; s += ''; s += ''; } var dsX = W - P - 96; var dsItems = [ {t:'hl', k:t('cop_vs_ds'), v:(dsApprox?'~':'')+pct(dsVal), c:(dsVal===null?TH.sub:(dsVal>=0?TH.green:TH.red))}, {t:'sep'}, {t:'kv', k:'Formula', v:'(Measured - Ref) / Ref', c:TH.sub}, {t:'kv', k:'Measured', v:fmt1(dsMeasured), c:TH.text}, {t:'kv', k:'Ref A7/W35', v:fmt1(dsRef), c:TH.sub}, {t:'kv', k:'Delta', v:(dsVal>=0?'+':'')+fmt1(dsMeasured-dsRef), c:TH.text} ]; var dsPop = popAttr({title:t('cop_vs_ds'), items:dsItems}); s += ''; s += ''; s += T(dsX+48, 27, t('cop_vs_ds'), {a:'middle', s:7.5, w:900, f:TH.sub}); s += T(dsX+48, 47, (dsApprox?'~':'')+pct(dsVal), {a:'middle', s:16, w:900, f:(dsVal===null?TH.sub:(dsVal>=0?TH.green:TH.red)), halo:1}); if (dsApprox && dsRef !== null) { s += T(dsX+48, 59, 'ref '+fmt1(dsRef)+' (A7/W35)', {a:'middle', s:6, w:800, f:TH.sub}); } else if (dsVal !== null) { s += T(dsX+48, 59, arrow(dsVal), {a:'middle', s:9, w:900, f:(dsVal>=0?TH.green:TH.red)}); } s += ''; /* ROW 2: Trend */ var tY = 72, tH = 80, tPadL = 28, tPadR = 8, tPadT = 4, tPadB = 14; var tPlotW = W - tPadL - tPadR, tPlotH = tH - tPadT - tPadB; var allVals = [modeData.heating.day, modeData.heating.week, modeData.heating.month, modeData.dhw.day, modeData.dhw.week, modeData.dhw.month, modeData.cooling.day, modeData.cooling.week, modeData.cooling.month].filter(hasN); if (allVals.length > 0) { var tMin = Math.max(0, Math.min.apply(null, allVals) - 0.3); var tMax = Math.max.apply(null, allVals) + 0.3; var tRng = Math.max(0.5, tMax - tMin); var hX = [tPadL+40, tPadL+tPlotW/2, tPadL+tPlotW-30]; var hy = function(v){ return tY + tPadT + tPlotH - ((v-tMin)/tRng)*tPlotH; }; for (var gy=0; gy<=2; gy++) { var gv2 = tMin + (tRng*gy/2); var gyy2 = hy(gv2); s += ''; s += T(tPadL-3, gyy2+3, gv2.toFixed(1), {a:'end', s:7, w:800, f:TH.sub}); } ['heating','dhw','cooling'].forEach(function(mk){ var md = modeData[mk]; var vals = [md.day, md.week, md.month]; var valid = []; for (var pi2=0; pi2<3; pi2++) if (hasN(vals[pi2])) valid.push(pi2); if (valid.length === 0) return; if (valid.length >= 2) { var mp = ''; for (var pv=0; pv'; } for (var pv2=0; pv2<3; pv2++) { if (!hasN(vals[pv2])) continue; var statsi = modeData[mk].stats; var dotItems = [ {t:'hl', k:MODE_ICON[mk]+' '+mk, v:fmt1(vals[pv2]), c:MODE_COL[mk]}, {t:'sep'}, {t:'kv', k:['Day','Week','Month'][pv2], v:fmt1(vals[pv2]), c:MODE_COL[mk]} ]; if (statsi) { if (hasN(statsi.n_samples)) dotItems.push({t:'kv', k:t('r_samples'), v:String(statsi.n_samples), c:TH.sub}); if (hasN(statsi.cop_min) && hasN(statsi.cop_max)) dotItems.push({t:'kv', k:'Range', v:fmt1(statsi.cop_min)+'–'+fmt1(statsi.cop_max), c:TH.text}); } var dotPop = popAttr({title:MODE_ICON[mk]+' '+mk, items:dotItems}); s += ''; s += ''; s += ''; s += ''; } }); s += T(hX[0], tY+tH-2, (LANG==='nl'?'dag':FR('day')), {a:'middle', s:7, w:800, f:TH.sub}); s += T(hX[1], tY+tH-2, (LANG==='nl'?'week':FR('week')), {a:'middle', s:7, w:800, f:TH.sub}); s += T(hX[2], tY+tH-2, (LANG==='nl'?'maand':FR('month')), {a:'middle', s:7, w:800, f:TH.sub}); } else { s += T(W/2, tY+tH/2, t('no_cop_data'), {a:'middle', s:9, w:900, f:TH.sub}); } /* ROW 3: Mode cards */ var mY = 122, cardH = 48; ['heating','dhw','cooling'].forEach(function(mk, i){ var mc = modeData[mk]; var cx2 = P + (i%2) * (CW + P); var cy2 = mY + Math.floor(i/2) * (cardH + 4); var cardItems = [ {t:'hl', k:MODE_ICON[mk]+' '+mk, v:fmt1(mc.day), c:MODE_COL[mk]}, {t:'sep'}, {t:'kv', k:'Day', v:fmt1(mc.day), c:MODE_COL[mk]}, {t:'kv', k:'Week', v:fmt1(mc.week), c:MODE_COL[mk]}, {t:'kv', k:'Month', v:fmt1(mc.month), c:MODE_COL[mk]} ]; if (mc.stats) { cardItems.push({t:'sep'}); if (hasN(mc.stats.n_samples)) cardItems.push({t:'kv', k:t('r_samples'), v:String(mc.stats.n_samples), c:TH.sub}); if (hasN(mc.stats.cop_min) && hasN(mc.stats.cop_max)) cardItems.push({t:'kv', k:'Range', v:fmt1(mc.stats.cop_min)+'–'+fmt1(mc.stats.cop_max), c:TH.text}); if (hasN(mc.stats.cop_p10) && hasN(mc.stats.cop_p90)) cardItems.push({t:'kv', k:'p10–p90', v:fmt1(mc.stats.cop_p10)+'–'+fmt1(mc.stats.cop_p90), c:TH.text}); } var cardPop = popAttr({title:MODE_ICON[mk]+' '+mk.toUpperCase(), items:cardItems}); var dim = !hasN(mc.day); s += ''; s += ''; s += ''+MODE_ICON[mk]+''; s += T(cx2+22, cy2+13, mk.toUpperCase().slice(0,4), {s:7, w:900, f:TH.sub}); if (hasN(mc.day) && hasN(mc.month) && mc.month !== 0) { var trdPct = ((mc.day - mc.month) / mc.month * 100); var trdCol = trdPct > 2 ? TH.green : trdPct < -2 ? TH.red : TH.sub; var trdIcon = trdPct > 2 ? '▲' : trdPct < -2 ? '▼' : '●'; s += T(cx2+CW-6, cy2+13, trdIcon+' '+Math.abs(trdPct).toFixed(0)+'%', {a:'end', s:7, w:900, f:trdCol}); } s += T(cx2+6, cy2+34, fmt1(mc.day), {s:16, w:900, f:(hasN(mc.day)?MODE_COL[mk]:TH.sub), halo:1}); if (hasN(mc.day) && hasN(mc.week) && hasN(mc.month)) { var msX = cx2 + CW*0.45, msY = cy2 + 20, msW = CW*0.5, msH = 16; var msVals = [mc.month, mc.week, mc.day]; var msMin = Math.min.apply(null, msVals), msMax = Math.max.apply(null, msVals); var msRng = Math.max(0.05, msMax - msMin); var msPath = ''; for (var mi2=0; mi2<3; mi2++) { var mx = msX + (mi2/2)*msW; var my = msY + msH - ((msVals[mi2]-msMin)/msRng)*msH; msPath += (mi2===0?'M':' L') + mx.toFixed(1) + ',' + my.toFixed(1); } s += ''; for (var mi3=0; mi3<3; mi3++) { var mx3 = msX + (mi3/2)*msW; var my3 = msY + msH - ((msVals[mi3]-msMin)/msRng)*msH; s += ''; } } s += ''; }); var cxComb = P + (CW + P); var cyComb = mY + cardH + 4; var combItems = [ {t:'hl', k:'⚡ COMBINED', v:fmt1(copComb), c:TH.accent}, {t:'sep'}, {t:'kv', k:'heating', v:fmt1(modeData.heating.day), c:TH.red}, {t:'kv', k:'dhw', v:fmt1(modeData.dhw.day), c:TH.blue} ]; var combPop = popAttr({title:'⚡ Combined COP', items:combItems}); s += ''; s += ''; s += '⚡'; s += T(cxComb+22, cyComb+13, 'COMB', {s:7, w:900, f:TH.sub}); s += T(cxComb+6, cyComb+34, fmt1(copComb), {s:16, w:900, f:TH.accent, halo:1}); s += ''; /* ROW 4: Combined Chart */ var cY = 244, cH = 120; var cPadL = 22, cPadR = 22, cPadT = 12, cPadB = 14; var cPlotW = W - cPadL - cPadR, cPlotH = cH - cPadT - cPadB; var durVals = sub.map(function(d){ return d.dur_avg || 0; }); var cntVals = sub.map(function(d){ return d.cycles || 0; }); var durMaxActual = Math.max.apply(null, durVals.concat([targetDur*1.2, 60])); var durYMax = Math.ceil(durMaxActual / 60) * 60; var cntMaxActual = Math.max.apply(null, cntVals.concat([targetCycles*1.5, 4])); var cntYMax = Math.ceil(cntMaxActual * 1.2); var slotW = cPlotW / sub.length; var barW = Math.max(2, Math.min(slotW*0.55, 12)); var durY = function(v){ return cY + cPadT + cPlotH - (v/durYMax)*cPlotH; }; var cntY = function(v){ return cY + cPadT + cPlotH - (v/cntYMax)*cPlotH; }; var slotCx = function(i){ return cPadL + (i+0.5)*slotW; }; var gridParts = ''; for (var g=0; g<=2; g++){ var gyc = cY + cPadT + cPlotH - (g/2)*cPlotH; var dval = Math.round((g/2)*durYMax); var cval = Math.round((g/2)*cntYMax); gridParts += ''; gridParts += T(cPadL-3, gyc+3, dval, {a:'end', s:6.5, w:800, f:TH.text}); gridParts += T(cPadL+cPlotW+3, gyc+3, cval, {s:6.5, w:800, f:TH.purple}); } var tgtParts = ''; if (targetDur > 0 && targetDur <= durYMax) { var tdY = durY(targetDur); tgtParts += ''; } if (targetCycles > 0 && targetCycles <= cntYMax) { var tcY = cntY(targetCycles); tgtParts += ''; } var barsParts = '', linePath = '', dotsParts = ''; var latestIdx = sub.length - 1; for (var i2=0; i2= 70) || (!hasN(q) && d.cycles <= targetCycles && durVals[i2] >= targetDur); var isBad = (hasN(q) && q < 40) || (!hasN(q) && (d.cycles > targetCycles * 1.5 || durVals[i2] < 20)); var badge = isBad ? '⚠️' : (isGood ? '👑' : ''); var barItems = [ {t:'hl', k:t('r_avgdur'), v:fmtMin(d.dur_avg), c:bcol}, {t:'sep'}, {t:'kv', k:t('r_cycles'), v:String(d.cycles), c:TH.purple}, {t:'kv', k:t('r_totaldur'), v:fmtMin(d.dur_total_min), c:TH.text}, {t:'kv', k:t('r_maxdur'), v:fmtMin(d.dur_max_min), c:TH.text}, {t:'sep'}, {t:'kv', k:t('r_quality'), v:(q===null?'—':vI(q)+'%'), c:qColor(q)}, {t:'kv', k:t('r_buh'), v:String(d.buh||0), c:d.buh>0?TH.buh:TH.sub}, {t:'kv', k:t('r_defrost'), v:String(d.defrost||0), c:d.defrost>0?TH.defrost:TH.sub} ]; var barPop = popAttr({title:weekday(d.day)+' '+fmtDay(d.day), items:barItems, modes:buildModeChips(d)}); barsParts += ''; if (bh > 0.5) { barsParts += ''; if (isToday2) { barsParts += ''; } } if (badge) barsParts += ''+badge+''; barsParts += ''; barsParts += ''; var ly = cntY(cntVals[i2]); linePath += (i2===0?'M':' L') + bxc.toFixed(1) + ',' + ly.toFixed(1); dotsParts += ''; if (isToday2) { dotsParts += ''; } } s += T(cPadL, cY+cPadT-3, 'm', {s:6.5, w:900, f:TH.text}); s += T(cPadL+cPlotW, cY+cPadT-3, (LANG==='nl'?'cycli':FR('cycles')), {a:'end', s:6.5, w:900, f:TH.purple}); s += gridParts + tgtParts + barsParts; if (sub.length >= 2) { s += ''; } s += dotsParts; s += ''; var labelStep = Math.max(1, Math.ceil(sub.length/7)); for (var i3=0; i3'; s += T(cPadL+11, legY-1, (LANG==='nl'?'duur':FR('duration')), {s:6.5, w:800, f:TH.sub}); s += ''; s += T(cPadL+76, legY-1, (LANG==='nl'?'cycli':FR('cycles')), {s:6.5, w:800, f:TH.sub}); /* ROW 5: Strip */ var sY = 376, stripH = 40; var stripDefs = [ {lbl:t('spf_season'), val:spf, txt:(hasN(spf)?fmt1(spf):'—'), col:TH.purple, items:[{t:'hl', k:t('spf_season'), v:(hasN(spf)?fmt1(spf):'—'), c:TH.purple},{t:'sep'},{t:'kv',k:'Target',v:'≥3.5',c:TH.sub}]}, {lbl:t('week_trend'), val:degr, txt:(hasN(degr)?(degr>0?'+':'')+degr.toFixed(1).replace('.',',')+'%':'—'), col:(degr===null?TH.sub:(degr>=0?TH.green:TH.orange)), items:[{t:'hl', k:t('week_trend'), v:(hasN(degr)?(degr>0?'+':'')+degr.toFixed(1)+'%':'—'), c:(degr===null?TH.sub:(degr>=0?TH.green:TH.orange))},{t:'sep'},{t:'kv',k:'Threshold',v:'-8%',c:TH.sub},{t:'kv',k:'Critical',v:'-15%',c:TH.sub}]}, {lbl:t('buh_impact'), val:buhRt, txt:(hasN(buhRt)?fmtMin(buhRt/60):'—'), col:TH.buh, items:[{t:'hl', k:t('buh_impact'), v:(hasN(buhRt)?fmtMin(buhRt/60):'—'), c:TH.buh},{t:'sep'},{t:'kv',k:'Seconds',v:(hasN(buhRt)?String(Math.round(buhRt)):'—'),c:TH.text}]}, {lbl:t('defrost_n'), val:defrCnt, txt:(hasN(defrCnt)?Math.round(defrCnt)+'×':'—'), col:TH.defrost, items:[{t:'hl', k:t('defrost_n'), v:(hasN(defrCnt)?Math.round(defrCnt)+'×':'—'), c:TH.defrost},{t:'sep'},{t:'kv',k:FR('Last 7d'),v:'—',c:TH.sub}]} ]; var sw = (W - P*2 - 3*3) / 4; for (var j=0; j<4; j++) { var st = stripDefs[j]; var sx = P + j*(sw+3); var stripPop = popAttr({title:st.lbl, items:st.items}); s += ''; s += ''; s += T(sx+sw/2, sY+11, st.lbl, {a:'middle', s:6.5, w:900, f:TH.sub}); s += T(sx+sw/2, sY+28, st.txt, {a:'middle', s:12, w:900, f:st.col, halo:1}); s += ''; } /* ROW 6: Insight */ var insight = null; if (hasN(modeData.heating.day) && dsRef !== null && modeData.heating.day < dsRef * 0.85) { insight = {bad:true, text:(LANG==='nl'?'COP heating '+fmt1(modeData.heating.day)+' onder datasheet':FR('COP heating ')+fmt1(modeData.heating.day)+FR(' below datasheet')), items:[{t:'hl', k:'Heating COP', v:fmt1(modeData.heating.day), c:TH.red},{t:'kv',k:'Ref',v:fmt1(dsRef),c:TH.sub}]}; } else if (hasN(degr) && degr < -8) { insight = {bad:true, text:(LANG==='nl'?'COP daalt '+degr.toFixed(1)+'% week':FR('COP dropping ')+degr.toFixed(1)+FR('% week')), items:[{t:'hl', k:'Week trend', v:degr.toFixed(1)+'%', c:TH.red},{t:'kv',k:'Threshold',v:'-8%',c:TH.sub}]}; } else if (cntVals.some(function(v){ return v > targetCycles * 1.5; })) { insight = {bad:true, text:(LANG==='nl'?'Te veel cycli (target '+targetCycles+')':FR('Too many cycles (target ')+targetCycles+')'), items:[{t:'hl', k:'Peak cycles', v:String(Math.max.apply(null, cntVals)), c:TH.red},{t:'kv',k:'Target',v:String(targetCycles),c:TH.sub}]}; } else if (hasN(spf) && spf >= 3.5) { insight = {bad:false, text:(LANG==='nl'?'SPF '+fmt1(spf)+' ruim boven doel':FR('SPF ')+fmt1(spf)+FR(' well above target')), items:[{t:'hl', k:'SPF', v:fmt1(spf), c:TH.green},{t:'kv',k:'Target',v:'≥3.5',c:TH.sub}]}; } else if (hasN(copToday) && dsRef !== null && copToday > dsRef) { insight = {bad:false, text:(LANG==='nl'?'COP boven datasheet referentie':FR('COP above datasheet reference')), items:[{t:'hl', k:'COP', v:fmt1(copToday), c:TH.green},{t:'kv',k:'Ref',v:fmt1(dsRef),c:TH.sub}]}; } else { insight = {bad:false, text:(LANG==='nl'?'Systeem presteert normaal':FR('System performing normally')), items:[{t:'hl', k:'COP', v:fmt1(copToday), c:TH.green}]}; } var insPop = popAttr({title:(insight.bad?'⚠️ ':'✅ ')+insight.text, items:insight.items}); s += ''; s += ''; s += T(P+8, 439, (insight.bad?'⚠️ ':'✅ ')+insight.text, {s:8, w:900, f:(insight.bad?TH.red:TH.green)}); s += ''; return s; }; /* BUILD WIN VIEW */ var buildWinView = function(winSize){ var sub = daysAll.slice(-winSize); var wkey = 'w'+winSize; var cls = String(winSize)===activeWin ? ' pa-view-active' : ''; if(sub.length === 0){ return '
⏳ '+(LANG==='nl'?'Geen data':FR('No data'))+'
'; } var wTotCycles = sub.reduce(function(a,d){ return a + d.cycles; }, 0); var wAvgQ = (function(){ var q = sub.filter(function(d){ return d.quality_avg !== null; }); return q.length>0 ? q.reduce(function(a,d){ return a+d.quality_avg; },0)/q.length : null; })(); var wAvgDur = (function(){ var tot=0, cnt=0; sub.forEach(function(d){ tot += d.dur_total_min; cnt += d.cycles; }); return cnt>0 ? tot/cnt : 0; })(); var half = Math.floor(sub.length/2); var _avgQ = function(arr){ var q = arr.filter(function(d){ return d.quality_avg !== null; }); return q.length>0 ? q.reduce(function(a,d){ return a+d.quality_avg; },0)/q.length : null; }; var qF = _avgQ(sub.slice(0, half)), qS = _avgQ(sub.slice(half)); var trendD = (qF !== null && qS !== null) ? (qS - qF) : null; var windowSet = {}; sub.forEach(function(d){ windowSet[d.day] = true; }); var distCounts = [0,0,0,0]; rawDays.forEach(function(rd){ if(!rd.day || !windowSet[rd.day] || !rd.cycles || rd.cycles <= 0) return; var avg = rdDurAvg(rd); if(!isFinite(avg) || avg <= 0) return; var idx = avg<20?0:avg<60?1:avg<180?2:3; distCounts[idx] += rd.cycles; }); var insightIcon='📊', insightText=(LANG==='nl'?'Onvoldoende data':FR('Insufficient data')), insightCol=TH.sub, insightBg='rgba(128,128,128,0.08)'; if(distCounts[0] >= 3 && distCounts[0] >= distCounts[2]){ insightIcon='🚨'; insightText=distCounts[0]+(LANG==='nl'?' cycli in PENDEL zone':FR(' cycles in PENDULUM zone')); insightCol=TH.red; insightBg='rgba(198,40,40,0.10)'; } else if(distCounts[2] >= 3 && distCounts[0] <= 1){ insightIcon='✅'; insightText=distCounts[2]+(LANG==='nl'?' cycli in IDEAAL zone':FR(' cycles in IDEAL zone')); insightCol=TH.green; insightBg='rgba(46,125,50,0.10)'; } else if(wAvgQ !== null && wAvgQ >= 70){ insightIcon='⭐'; insightText=(LANG==='nl'?'Gemiddelde kwaliteit ':FR('Average quality '))+vI(wAvgQ)+'%'; insightCol=TH.green; insightBg='rgba(46,125,50,0.08)'; } else if(wAvgQ !== null && wAvgQ < 40){ insightIcon='⚠️'; insightText=(LANG==='nl'?'Gemiddelde kwaliteit laag (':FR('Average quality low ('))+vI(wAvgQ)+'%)'; insightCol=TH.red; insightBg='rgba(198,40,40,0.08)'; } else if(trendD !== null && trendD > 5){ insightIcon='📈'; insightText=(LANG==='nl'?'Kwaliteit stijgt (+':FR('Quality rising (+'))+vI(trendD)+'%)'; insightCol=TH.green; insightBg='rgba(46,125,50,0.08)'; } else if(trendD !== null && trendD < -5){ insightIcon='📉'; insightText=(LANG==='nl'?'Kwaliteit daalt (':FR('Quality dropping ('))+vI(trendD)+'%)'; insightCol=TH.orange; insightBg='rgba(230,81,0,0.08)'; } else if(wTotCycles > 0){ insightIcon='🎯'; insightText=wTotCycles+(LANG==='nl'?' cycli over ':FR(' cycles over '))+sub.length+'d · Ø '+fmtMin(wAvgDur); insightCol=TH.text; insightBg='rgba(21,101,192,0.08)'; } var copHdrVal = getState(resolveEntity('cop_today')); var hd = function(tab, title, right){ return '
'+title+''+right+' · '+sub.length+(LANG==='fr'?' j':'d')+'
'; }; var hdrs = hd('dist', t('hdr_dist'), wTotCycles+'× · Ø '+fmtMin(wAvgDur)) + hd('scatter', t('hdr_scatter'), t('cnt_perday')) + hd('cop', t('hdr_cop'), 'COP '+fmt1(copHdrVal)); return '
' + hdrs + '
' + ''+insightIcon+'' + ''+insightText+'' + '
' + ''+buildDistContent(sub, wkey)+'' + ''+buildScatterContent(sub, wkey)+'' + ''+buildCopContent(sub, wkey)+'' + '
'; }; var chip = function(color, bg, label){ return ''+label+''; }; var legendDist = '
' + chip(TH.red, DARK?'rgba(239,83,80,0.10)':'#FFEBEE', (LANG==='nl'?'PENDEL <20m':FR('PENDULUM <20m'))) + chip(TH.orange, DARK?'rgba(255,167,38,0.10)':'#FFF3E0', FR('SHORT 20-60m')) + chip(TH.green, DARK?'rgba(102,187,106,0.10)':'#E8F5E9', FR('GOOD 60-180m')) + chip(TH.blue, DARK?'rgba(66,165,245,0.10)':'#E3F2FD', FR('LONG >180m')) + '
'; var legendScatter = '
' + chip(TH.red, DARK?'rgba(239,83,80,0.10)':'#FFEBEE', '🚨 pendel') + chip(TH.orange, DARK?'rgba(255,167,38,0.10)':'#FFF3E0', '⚠️ mixed') + chip(TH.green, DARK?'rgba(102,187,106,0.10)':'#E8F5E9', '✅ ideaal') + '
'; var tabDefs = [{id:'dist', label:t('tab_dist')}, {id:'scatter', label:t('tab_scatter')}, {id:'cop', label:t('tab_cop')}]; var tabBar = '
'; for(var ti=0; ti'+td.label+'
'; } tabBar += ''; var winGroup = '
'; [7,15,30].forEach(function(w){ var act = (String(w) === activeWin) ? ' pa-active' : ''; winGroup += '
'+w+(LANG==='fr'?' j':'d')+'
'; }); winGroup += '
'; var actions = '
' + winGroup + '
'; var notes = '
' + '
'+t('note_dist')+'
' + '
'+t('note_scatter')+'
' + '
'+t('note_cop')+'
' + '
'; var css = ''; var r = '
'; r += css; r += tabBar; r += actions; r += buildWinView(7); r += buildWinView(15); r += buildWinView(30); r += '
' + legendDist + legendScatter + '
'; r += notes; r += '
'; r += '
'; r += '
'; return r; } catch(e) { return '
⚠️ Pattern error: ' + (e && e.message ? e.message : e) + '
'; } ]]]