${it(this._t("lbl.program_tip",{},"Override which profile is matched to the current cycle. Auto-detect lets the integration pick the best match automatically. Pin a specific program to force-match it when auto-detect is wrong or you know what is running."))}
${b}
`,k=[];t.recording&&this._canEdit()&&k.push(`
\u25CF
${this._t("msg.recording_in_progress",{},"Recording in progress")}
Live Match Debug ${it("Confidence: how closely the current power curve matches the top candidate profile (0-100%). Ambiguous: the two best candidates score within 5% of each other - the label is uncertain until the cycle finishes.")}
${this._t("hdr.manual_recording",{},"Manual Recording")}${it(this._t("hdr.manual_recording_tip",{},"Run a cycle intentionally while WashData records the power trace. Start just before the appliance starts, Stop when it finishes, then Process to save it as a named profile."))}${o?`${o}`:""}
${i}
`}_htmlHistory(){const t=this._cycles||[],e=this._refCycles||[],s=e.concat(t),r=this._canEdit(),n=this._selectMode&&r,a=this._cycleSel,{col:o,dir:i}=this._cycleSort,{text:l,status:c}=this._cycleFilter,_=this._mlById||{},p=new Set((this._feedbacks||[]).map(w=>w.cycle_id)),g=w=>_[w.id],u=w=>{const P=g(w);return!!(P&&P.ml_review&&P.ml_review.reviewed_at)},v=w=>{const P=g(w);return!!(P&&P.ml_review&&P.ml_review.golden)},b=w=>{if(p.has(w.id))return!0;if(u(w))return!1;const P=g(w),z=P&&P.ml_quality_label;return["uncertain","review"].includes(z)||["force_stopped","interrupted"].includes(w.status)},$=s.filter(b).length;let k=s;if(l){const w=l.toLowerCase();k=k.filter(P=>(P.profile_name||P.matched_profile||"").toLowerCase().includes(w))}c==="unlabelled"?k=k.filter(w=>!w.profile_name&&!w.matched_profile):c==="needs_review"?k=k.filter(b):c==="imported"?k=k.filter(w=>w.is_reference):c&&(k=k.filter(w=>(w.status||"completed")===c));const S={date:w=>w.start_time?new Date(w.start_time).getTime():0,confidence:w=>w.match_confidence,duration:w=>w.duration,energy:w=>w.energy_kwh!=null?w.energy_kwh:w.energy_wh!=null?w.energy_wh/1e3:null,cost:w=>w.cost!=null?w.cost:-1,status:w=>w.status||"completed",profile:w=>(w.profile_name||w.matched_profile||"\uFFFF").toLowerCase()};k=Qt(k,S[o]||S.date,i);const C=w=>w==="completed"?"var(--success-color, #4caf50)":w==="interrupted"?"var(--error-color, #f44336)":w==="force_stopped"?"var(--warning-color, #ff9800)":"var(--secondary-text-color)",R=w=>{if(!w.is_reference)return"";const P=w.cycle_origin==="backfill",z=P?this._t("badge.backfilled_tip",{},"Detected in imported power history. Shapes program matching only, not counted in stats."):this._t("badge.imported_tip",{},"Imported from the community store. Used for matching only, not counted in stats.");return` ${P?"\u{1F557}":"\u{1F4E5}"}`},B=w=>v(w)?' \u2B50':p.has(w.id)?' \u{1F4AC}':u(w)?' \u2713':b(w)?' \u25CF':"",W=w=>{if(w.anomaly!=="overrun")return"";const P=w.overrun_ratio?" "+this._t("badge.overrun_ratio",{x:Number(w.overrun_ratio).toFixed(1)},`(${Number(w.overrun_ratio).toFixed(1)}x expected)`):"";return` \u23F1`},O=w=>{if(w.anomaly!=="underrun")return"";const P=w.underrun_ratio?" "+this._t("badge.underrun_ratio",{pct:Math.round(w.underrun_ratio*100)},`(${Math.round(w.underrun_ratio*100)}% of expected)`):"";return` \u26A1`},D=w=>{if(!w.energy_anomaly||w.energy_anomaly==="none")return"";const P=w.energy_anomaly==="energy_spike",z=w.energy_z_score!=null?` (${w.energy_z_score>0?"+":""}${Number(w.energy_z_score).toFixed(1)}\u03C3)`:"",F=P?"badge.energy_spike":"badge.energy_low",K=P?"Higher energy than usual":"Lower energy than usual",Z=P?"\u{1F53A}":"\u{1F53B}",rt=P?"var(--error-color,#f44336)":"var(--info-color,#2196f3)";return` ${Z}`},A=w=>{const P=Array.isArray(w.artifacts)?w.artifacts.length:0;return P?` 1?"ies":"y"} detected (e.g. door opened mid-cycle) \u2014 open to see them on the graph`))}" style="color:var(--warning-color,#ff9800)">\u26A0`:""},E=w=>{const P=Array.isArray(w.restart_gaps)?w.restart_gaps.length:0;return P?` 1?"s":""} during this cycle \u2014 power trace has a hole`))}" style="color:var(--info-color,#2196f3)">\u21BB`:""},M=this._hass&&this._hass.config&&this._hass.config.currency||"",H=w=>w.cost!=null?`${w.cost.toFixed(2)}${M?" "+M:""}`:"-",q=k.map(w=>{const P=w.profile_name||w.matched_profile,z=w.match_confidence!=null?w.match_confidence*100:null,F=w.status||"completed",K=w.energy_kwh!=null?w.energy_kwh:w.energy_wh!=null?w.energy_wh/1e3:null,Z=n,rt=Z?``:``,ht={completed:this._t("status.completed",{},"Completed"),interrupted:this._t("status.interrupted",{},"Interrupted"),force_stopped:this._t("status.force_stopped",{},"Force stopped"),active:this._t("status.active",{},"Active")}[F]||F,nt=`${R(w)}${B(w)}${W(w)}${O(w)}${D(w)}${A(w)}${E(w)}`.trim();return`
${ft(this._t("lbl.profile",{},"Profile"),"profile",o==="profile",i,"cycsort","",this._t("col.profile_tip",{},"Matched program name. Unlabelled means no profile matched at end of cycle."))}
${this._t("lbl.flags",{},"Flags")}
${ft(this._t("lbl.status",{},"Status"),"status",o==="status",i,"cycsort","",this._t("col.status_tip",{},"Cycle outcome: Completed (natural end), Interrupted (abrupt power drop), Force Stopped (manual), or Needs Review (feedback pending)."))}
${ft(this._t("lbl.date",{},"Date"),"date",o==="date",i,"cycsort","",this._t("col.date_tip",{},"Date and time the cycle started."))}
${ft(this._t("lbl.duration",{},"Duration"),"duration",o==="duration",i,"cycsort","right",this._t("col.duration_tip",{},"Total cycle run time from start to end."))}
${ft(this._t("lbl.energy",{},"Energy"),"energy",o==="energy",i,"cycsort","right",this._t("col.energy_tip",{},"Total energy consumed (kWh). Computed by integrating power over time."))}
${ft(this._t("lbl.cost",{},"Cost"),"cost",o==="cost",i,"cycsort","right",this._t("col.cost_tip",{},"Energy cost for this cycle, frozen at completion using the price in effect then (energy x price per kWh). Set a price under Settings to populate it."))}
${ft(this._t("lbl.confidence",{},"Confidence"),"confidence",o==="confidence",i,"cycsort","right",this._t("col.confidence_tip",{},"Profile match confidence (0-100%). How closely the cycle power curve matched the identified program."))}
${s.length?this._t("msg.no_cycles_match",{},"No cycles match the current filter."):this._t("msg.no_cycles_yet",{},"No cycles recorded yet.")}
`:`
${Q}${q}
`}
${L}
`;return(this._cyclesError?`
${this._t("msg.fetch_error",{},"Failed to load data.")}
`:"")+j}_trendIcon(t){return t==="up"?`\u2191`:t==="down"?`\u2193`:""}_profileCardHtml(t){const e=t.avg_duration?this._t("lbl.duration_avg",{v:Math.round(t.avg_duration/60)},`~${Math.round(t.avg_duration/60)}m avg`):this._t("lbl.no_duration",{},"no duration"),s=t.avg_energy!=null?` \xB7 ${gt(t.avg_energy)}/cycle`:"",r=t.avg_energy!=null&&t.cycle_count?` \xB7 ${gt(t.avg_energy*t.cycle_count)} total`:"",n=this._hass&&this._hass.config&&this._hass.config.currency||"",a=t.avg_cost!=null?` \xB7 ${this._t("lbl.avg_cost",{},"Avg")} ${t.avg_cost.toFixed(2)}${n?" "+n:""}/${this._t("lbl.per_cycle_short",{},"cycle")}`:"",o=(this._profileHealth||{})[t.name],i=(this._profileTrends||{})[t.name];let l="";o&&o.health_status==="poor"?l=`\u26A0 ${this._t("badge.poor_fit",{},"poor fit")}`:o&&o.health_status==="fair"&&(l=`${this._t("badge.fair_fit",{},"fair fit")}`);let c="";if(i){const k=this._trendIcon(i.duration_trend),S=i.energy_trend?this._trendIcon(i.energy_trend):"";if(i.duration_trend!=="stable"||i.energy_trend==="up"){const C=[];if(i.duration_trend!=="stable"){const B=`${i.duration_slope_pct>0?"+":""}${i.duration_slope_pct}`;C.push(i.duration_trend==="up"?this._t("msg.duration_trend_up_tip",{pct:B},`Duration up (${B}%/cycle)`):this._t("msg.duration_trend_down_tip",{pct:B},`Duration down (${B}%/cycle)`))}if(i.energy_trend&&i.energy_trend!=="stable"){const B=`${i.energy_slope_pct>0?"+":""}${i.energy_slope_pct}`;C.push(i.energy_trend==="up"?this._t("msg.energy_trend_up_tip",{pct:B},`Energy up (${B}%/cycle)`):this._t("msg.energy_trend_down_tip",{pct:B},`Energy down (${B}%/cycle)`))}const R=C.join(", ")||this._t("msg.performance_trending",{},"Performance trending");c=`${k}${S||""}`}}const _=this._constants&&this._constants.PROFILE_MIN_WARMUP_CYCLES||5,p=o&&o.cycle_count||0,u=p<_&&!t.is_imported?`${this._t("msg.warmup_badge",{done:p,needed:_},`Still learning (${p}/${_} cycles)`)}`:"",v=t.is_imported?`\u{1F4E5} ${this._t("status.imported",{},"Imported")}`:"",b=[l,c,u,v].filter(Boolean).join(" "),$=Array.isArray(t.signature_curve)&&t.signature_curve.length>=3?``:"";return`
`}_paintSparkline(t,e){if(!t||!Array.isArray(e)||e.length<3)return;const s=(getComputedStyle(this).getPropertyValue("--primary-color")||"#03a9f4").trim()||"#03a9f4",r=window.devicePixelRatio||1,n=t.getBoundingClientRect(),a=t.width=Math.max(1,Math.round((n.width||64)*r)),o=t.height=Math.max(1,Math.round((n.height||20)*r)),i=t.getContext("2d");i.clearRect(0,0,a,o);const l=Math.max(...e,1),c=2*r,_=g=>c+(e.length===1?0:g/(e.length-1)*(a-2*c)),p=g=>o-c-Math.max(0,g)/l*(o-2*c);i.beginPath(),e.forEach((g,u)=>{const v=_(u),b=p(g);u===0?i.moveTo(v,b):i.lineTo(v,b)}),i.strokeStyle=s,i.lineWidth=1.5*r,i.lineJoin="round",i.lineCap="round",i.stroke()}_drawProfileSparklines(){const t=this.shadowRoot;if(!t)return;const e=t.querySelectorAll("canvas[data-spark-prof]");if(!e.length)return;const s={};for(const r of this._profiles||[])s[r.name]=r;e.forEach(r=>{const n=s[r.dataset.sparkProf];this._paintSparkline(r,n&&n.signature_curve||[])})}_htmlProfiles(){const t=this._busy.has("rebuild-envelopes"),e=this._canEdit(),s={};this._profiles.forEach(u=>{s[u.name]=u});const r=this._profileGroups||{groups:[],suggestions:[]},n=new Set;r.groups.forEach(u=>(u.members||[]).forEach(v=>n.add(v)));const a=r.groups.map(u=>{const v=(u.members||[]).map(C=>s[C]?this._profileCardHtml(s[C]):"").join(""),b=Math.round((u.cohesion!=null?u.cohesion:1)*100),$=u.cohesive?`${this._t("lbl.cohesion_good",{pct:b},"cohesion "+b+"%")}`:`${this._t("lbl.cohesion_low",{pct:b},"\u26A0 low cohesion "+b+"%")}`,k=u.cohesive?"":`
${this._t("msg.group_not_cohesive",{},"These profiles aren't similar enough to group reliably, so matching treats them individually until you remove the outlier or split the group.")}
\u{1F4E5} ${this._t("msg.onboard_download",{},"New device? Adopt a ready-made setup (programs, reference cycles and phases) from another WashData user with the same appliance.")}
${this._t("msg.profiles_intro",{},"Click a profile for stats, phases and cleanup. Group near-identical programs (same shape/duration, different temperature or spin) so matching reliably picks between them.")}
${e?`
`:""}
${a}
${this._profiles.length===0?`
\u{1F4CA}
${this._t("msg.no_profiles_yet",{},"No profiles yet. Create one from a labelled cycle.")}
${this._t("msg.group_modal_help",{},"Group programs with the same shape that differ in temperature/spin (durations may vary). Matching scores the group as one candidate, then picks the best-fitting member. Pick at least 2; the overlay shows how alike they are.")}
\u{1F4A1} ${this._t("msg.showing_suggestions",{count:W},`Showing ${W} setting${W>1?"s":""} with suggestions.`)} ${this._t("msg.show_all_settings",{},"Show all settings")}.
${D}
`:`
\u{1F4A1} ${this._t("msg.tuning_suggestions_available",{count:W},`${W} tuning suggestion${W>1?"s":""} available from observed cycles. They appear beside the relevant fields.`)}
${D}
${A}
`}_storeApplianceType(){return{washing_machine:"washer",washer:"washer",dryer:"dryer",dishwasher:"dishwasher",washer_dryer:"washer_dryer"}[this._opts.device_type]||""}_storeDeviceDeclared(){return!!((this._opts.store_brand||"").trim()&&(this._opts.store_model||"").trim())}_shareableByProgram(){const t=new Map;for(const s of this._shareableCycles||[]){const r=(s.profile_name||"").trim();r&&(t.has(r)||t.set(r,[]),t.get(r).push(s))}const e=Array.from(t.entries()).map(([s,r])=>({program:s,cycles:r})).sort((s,r)=>s.program.localeCompare(r.program));for(const s of this._shareAllPrograms||[])t.has(s)||e.push({program:s,cycles:[],noCycles:!0});return e}_catalogEntryKey(){const t=this._opts||{};return[(t.store_brand||"").trim().toLowerCase(),(t.store_model||"").trim().toLowerCase(),t.device_type||""].join("|")}_ensureCatalogEntry(){const t=this._catalogEntryKey(),e=this._catalogEntry;e&&e.key===t||(this._catalogEntry={key:t,brand:null,device:null,deviceId:null,loading:!0},this._loadCatalogEntry(t))}_catalogEntryFor(t,e){const s=this._catalogEntry;if(!s||s.key!==this._catalogEntryKey())return null;const r=e==="brand"?s.brand:s.device;if(!r)return null;const n=e==="brand"?r.brand:r.model;return String(n||"").toLowerCase()===String(t||"").trim().toLowerCase()?r:null}async _loadCatalogEntry(t){const e=this._devices[this._selIdx],s=this._opts||{},r=(s.store_brand||"").trim(),n=(s.store_model||"").trim();if(!e||!this._onlineEnabled()||!r||!n){this._catalogEntry&&this._catalogEntry.key===t&&(this._catalogEntry.loading=!1);return}let a=null;try{a=await this._ws({type:`${y}/store_get_catalog_entry`,entry_id:e.entry_id,brand:r,model:n,appliance_type:s.device_type||""})}catch{}!this._catalogEntry||this._catalogEntry.key!==t||(this._catalogEntry={key:t,loading:!1,brand:a&&a.brand||null,device:a&&a.device||null,deviceId:a&&a.device_id||null},this._isActiveEntry(e.entry_id)&&this._renderPreservingFormEdits())}_refreshComboAfterLoad(t,e,s=!0){const r=this.shadowRoot&&this.shadowRoot.getElementById(t);if(r&&this.shadowRoot.activeElement===r){s&&r.dispatchEvent(new Event("input",{bubbles:!0}));return}this._isActiveEntry(e)&&this._render()}_ensureCatalogList(t,e){if(this._onlineEnabled()){if(t==="store_brand"){this._ensureBrandCandidates(e);return}if(t==="store_model"){const s=String((this._opts||{}).store_brand||"").trim();if(!s)return;(this._catalog.forBrand!==s||this._catalog.devices===void 0)&&(this._catalog.forBrand=s,this._catalog.devices=null,this._loadCatalogDevices(s))}}}_ensureBrandCandidates(t){const e=String(t||"").trim().toLowerCase();if(this._catalog.brandsFull)return;if(!e){clearTimeout(this._brandSearchTimer),this._brandSearchTimer=null,this._catalog.brands===void 0&&(this._catalog.brands=null,this._loadCatalogBrands(""));return}(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).some(r=>e.startsWith(r))||(clearTimeout(this._brandSearchTimer),this._brandSearchTimer=setTimeout(()=>this._loadCatalogBrands(e),250))}_mergeBrandCandidates(t){const e=new Map((Array.isArray(this._catalog.brands)?this._catalog.brands:[]).map(s=>[String(s.id!=null?s.id:s.brand),s]));for(const s of t||[])e.set(String(s.id!=null?s.id:s.brand),s);this._catalog.brands=Array.from(e.values()).sort((s,r)=>String(s.brand||"").localeCompare(String(r.brand||"")))}async _loadCatalogBrands(t=""){this._entityListCache=this._entityListCache||{};let e=!1;const s=this._devices[this._selIdx];if(!s||!this._onlineEnabled()){this._catalog.brands=[],this._entityListCache.store_brand=[];return}if(!(t&&this._catalog.brandsFull)){this._catalog.brands===void 0&&(this._catalog.brands=null);try{const r=await this._ws({type:`${y}/store_list_brands`,entry_id:s.entry_id,query:t||null,include_pending:!this._catalog.approvedOnly}),n=r&&r.items||[];t?(this._mergeBrandCandidates(n),(this._catalog.brandPrefixes||(this._catalog.brandPrefixes=[])).push(t)):(this._catalog.brands=n,this._catalog.brandsFull=!0)}catch{Array.isArray(this._catalog.brands)||(this._catalog.brands=[]),e=!0}this._entityListCache.store_brand=(this._catalog.brands||[]).map(r=>r.brand).filter(Boolean),this._refreshComboAfterLoad("wd-store-brand",s.entry_id,!e)}}async _loadCatalogDevices(t){this._entityListCache=this._entityListCache||{};const e=this._devices[this._selIdx];if(!e||!this._onlineEnabled()||!t){this._catalog.devices=[],this._entityListCache.store_model=[];return}try{const s=await this._ws({type:`${y}/store_search_devices`,entry_id:e.entry_id,query:t,appliance_type:this._storeApplianceType(),include_pending:!this._catalog.approvedOnly});this._catalog.forBrand===t&&(this._catalog.devices=s&&s.items||[])}catch{this._catalog.forBrand===t&&(this._catalog.devices=[])}this._entityListCache.store_model=(this._catalog.devices||[]).map(s=>s.model).filter(Boolean),this._refreshComboAfterLoad("wd-store-model",e.entry_id)}async _loadShareProfiles(){const t=this._devices[this._selIdx],e=this._modal;if(!t||!e||e.type!=="store-share")return;const s=(this._opts.store_brand||"").trim(),r=(this._opts.store_model||"").trim();if(!s||!r){e.profiles=[],this._modal===e&&this._render();return}try{const n=await this._ws({type:`${y}/store_get_device_profiles`,entry_id:t.entry_id,brand:s,model:r,appliance_type:this._opts.device_type||""});if(this._modal!==e)return;e.profiles=n&&n.items||[],e.deviceId=n&&n.device_id||null}catch{this._modal===e&&(e.profiles=[])}this._modal===e&&this._render()}_htmlAutomations(){const t=this._deviceAutomations||[],e=t.length?t.map(n=>`\u{1F517} ${d(n.name)}${n.enabled?"":' (off)'}`).join(""):`${this._autoLoading?this._t("msg.loading",{},"Loading\u2026"):this._t("hdr.no_automations",{},"No automations reference this device yet.")}`,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[],r=s.length?`
${this._t("msg.legacy_actions_title",{count:s.length},`${s.length} legacy custom action${s.length>1?"s":""} still running`)}
${this._t("msg.old_actions_warning",{},"Configured with the old actions editor (now removed). They still fire on cycle events but can no longer be edited here. Convert them into a normal automation, or remove them.")}
`:"";return`
${this._t("hdr.automations",{},"Automations")}
${this._t("msg.automations_intro",{start:"ha_washdata_cycle_started",end:"ha_washdata_cycle_ended"},"WashData fires {start} / {end} events and exposes entities, so notifications and actions are best built as normal Home Assistant automations. Automations that use this device appear below.")}
`}async _loadDeviceAutomations(t){this._deviceAutomations=[];const e=this._hass;if(!(!e||!e.callWS))try{let s=null;const r=e.devices||{};for(const i of Object.values(r))if((i.config_entries||[]).includes(t)){s=i.id;break}const n=s?await e.callWS({type:"search/related",item_type:"device",item_id:s}):await e.callWS({type:"search/related",item_type:"config_entry",item_id:t});if(!this._isActiveEntry(t))return;const a=n&&n.automation||[],o=e.states||{};this._deviceAutomations=a.map(i=>{const l=o[i]&&o[i].attributes||{};return{entity_id:i,id:l.id,name:l.friendly_name||i,enabled:o[i]?o[i].state==="on":!0}}).filter(i=>i.id)}catch{this._isActiveEntry(t)&&(this._deviceAutomations=[])}}_navigate(t){try{history.pushState(null,"",t),this.dispatchEvent(new CustomEvent("location-changed",{bubbles:!0,composed:!0,detail:{replace:!1}}))}catch{try{window.location.assign(t)}catch{}}}async _newAutomationFromEvent(t){const e=this._devices[this._selIdx];if(!e)return;const s=this._hass,r=t==="started"?"ha_washdata_cycle_started":"ha_washdata_cycle_ended",n=t==="started"?"started":"finished",a={alias:`${e.title||"WashData"}: cycle ${n}`,description:`Runs when the WashData ${e.title||""} cycle ${n}. Add your actions (notify, lights, ...).`,mode:"single",trigger:[{platform:"event",event_type:r,event_data:{entry_id:e.entry_id}}],condition:[],action:[]},o="washdata_"+Date.now().toString(36);try{s&&s.callApi?(await s.callApi("POST","config/automation/config/"+o,a),this._navigate("/config/automation/edit/"+o)):this._navigate("/config/automation/edit/new")}catch(i){this._showToast(this._t("msg.toast_automation_failed",{error:i.message||i},"Could not create automation: "+(i.message||i)),"error")}}async _convertLegacyActions(){const t=this._devices[this._selIdx],e=this._hass,s=Array.isArray(this._opts.notify_actions)?this._opts.notify_actions:[];if(!t||!s.length)return;const r={alias:`${t.title||"WashData"}: migrated custom actions`,description:"Migrated from WashData legacy custom actions (which ran on cycle start, finish and live). Trim the triggers as needed. Note: old {device}/{duration}-style placeholders are NOT templated here - replace them with Jinja templates such as {{ trigger.event.data.device_name }}.",mode:"single",trigger:[{platform:"event",event_type:"ha_washdata_cycle_started",event_data:{entry_id:t.entry_id}},{platform:"event",event_type:"ha_washdata_cycle_ended",event_data:{entry_id:t.entry_id}}],condition:[],action:s},n="washdata_"+Date.now().toString(36);if(!e||!e.callApi){this._showToast(this._t("msg.toast_no_automation",{},"Cannot create automation here"),"error");return}let a=!1;try{await e.callApi("POST","config/automation/config/"+n,r),a=!0,await this._ws({type:`${y}/set_options`,entry_id:t.entry_id,options:{notify_actions:[]}}),this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:[]}),this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+n)}catch(o){const i=o.message||o;if(!a){this._showToast(this._t("msg.toast_convert_failed",{error:i},"Convert failed: "+i),"error");return}let l=null;try{const c=await this._ws({type:`${y}/get_options`,entry_id:t.entry_id}),_=c&&c.options||{},p=Array.isArray(_.notify_actions)?_.notify_actions:[];l=p.length>0,this._isActiveEntry(t.entry_id)&&(this._opts={...this._opts,notify_actions:p})}catch{l=null}if(l===!1){this._showToast(this._t("msg.toast_automation_migrated",{},"Actions migrated to an automation; opening editor")),this._navigate("/config/automation/edit/"+n);return}if(l===!0){try{await e.callApi("DELETE","config/automation/config/"+n),this._showToast(this._t("msg.toast_convert_rolled_back",{error:i},"Migration failed and was rolled back (no automation left behind): "+i),"error")}catch{this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}return}this._showToast(this._t("msg.toast_convert_orphan",{},"The automation was created, but clearing the old actions failed. Do not retry: remove the legacy actions manually to avoid a duplicate automation."),"error")}}_htmlSettingsSection(t){const e=this._opts&&this._opts.device_type||"",s=this._settingsLevel()==="basic",r=l=>!(l.id==="ml_training"||e&&l.notDeviceTypes&&l.notDeviceTypes.includes(e)||e&&l.onlyDeviceTypes&&!l.onlyDeviceTypes.includes(e)||s&&!this._secHasBasicFields(l)),n=mt.find(l=>l.id===this._settingsSec&&r(l))||mt.find(l=>r(l))||mt[0],a=n.intro||this._t("section."+n.id+".intro",{},"")?`
${this._t("msg.notify_services_hint",{entity:"notify.<name>",vars:""+d($t)+""},"Use {entity} service IDs (comma-separated for multiple). Template variables: {vars}.")}
${this._t("msg.no_settings_match",{q:e},`No settings match "${d(e)}"`)}
`}_mlSugKeys(t){const e=t||Object.assign({},this._optDefaults,this._opts,this._pendingSettings||{});return this._mlSugKeysFrom(this._mlSettings,e,this._lockedSuggestions)}_mlSugKeysFrom(t,e,s){const r=new Set(s||[]),n=e||{},a=new Set;for(const[o,i]of Object.entries(t||{}))r.has(o)||i&&i.ml_value!=null&&!Ot(i.ml_value,n[o])&&a.add(o);return a}_sugCountsForDevice(t){if(!t)return{classic:0,ml:0,total:0};const e=this._devices[this._selIdx],s=!!(e&&e.entry_id===t.entry_id),r=Array.isArray(t.suggestion_keys)?t.suggestion_keys:null,n=r?r.length:t.suggestions_count||0,a=s?Object.assign({},t.option_defaults||{},t.options||{},this._opts,this._pendingSettings||{}):Object.assign({},t.option_defaults||{},t.options||{}),o=this._mlSugKeysFrom(s?this._mlSettings:this._mlSettingsByEntry[t.entry_id],a,s?this._lockedSuggestions:this._lockedByEntry[t.entry_id]),i=r?[...o].filter(l=>!r.includes(l)).length:o.size;return{classic:n,ml:i,total:n+i}}_htmlSettingsSugOnly(t){const e=new Set((this._suggestions||[]).map(a=>a.key));for(const a of this._mlSugKeys(t))e.add(a);if(!e.size)return`
${this._t("msg.no_suggestions",{},"No active suggestions.")}
`;const s=this._opts&&this._opts.device_type||"",r=mt.filter(a=>!(a.id==="ml_training"||s&&a.notDeviceTypes&&a.notDeviceTypes.includes(s)||s&&a.onlyDeviceTypes&&!a.onlyDeviceTypes.includes(s)));let n="";for(const a of r){const i=(a.fields||(a.groups||[]).flatMap(c=>c.fields||[])).filter(c=>e.has(c.key));if(!i.length)continue;const l=i.map(c=>this._renderField(c,t)).filter(Boolean).join("");l&&(n+=`
${this._t("msg.ml_intro",{},"WashData ships with smart models that work out of the box.")}
${this._htmlMlStatusSection(e,r)}
${this._t("hdr.ml_settings_card",{},"Settings")}
${this._t("msg.ml_settings_intro",{},"Two independent switches: one applies the models while a cycle runs, the other lets WashData fine-tune them to your machine over time.")}
${this._t("msg.saving_triggers_reload",{},"Saving triggers an integration reload.")}
`;const n=Object.keys(t.on_device_models||{}).length,a=n?`${this._t("ml.personalized",{},"\u25CF Personalized to this machine")}${this._t("lbl.models_fine_tuned",{count:n},"(fine-tuned models: "+n+")")}`:`${this._t("ml.builtin_models",{},"\u25CF Using built-in models")}`,o=t.cycle_count||0,i=t.min_cycles||0,l=o>=i,c=i>0?Math.min(100,Math.round(o/i*100)):100,_=l?"var(--success-color,#4caf50)":"var(--warning-color,#ff9800)",p=Math.max(0,i-o),g=l?this._t("msg.enough_data",{current:o,min:i},`Enough data to learn from (${o}/${i} cycles).`):this._t("msg.collecting_data",{need:p,current:o,min:i},`Collecting data. Cycles still needed before fine-tuning can start: ${p} (${o}/${i}).`),u=`
`,v=t.last_trained?st(t.last_trained):"never",b=s?` ${this._t("status.fine_tuning",{},"fine-tuning now\u2026")}`:t.enabled?this._t("lbl.auto_fine_tune_on",{hour:String(t.hour).padStart(2,"0")},`auto fine-tune on (around ${String(t.hour).padStart(2,"0")}:00)`):this._t("lbl.auto_fine_tune_off",{},"auto fine-tune off");return`
${this._t("hdr.ml_learned",{},"What WashData has learned")}
${this._t("msg.ml_learned_intro",{},"Models fine-tuned to this machine.")}
${n}
`}_mlQualityChip(t){let e=0,s="",r=t.metric_key?this._t(t.metric_key,t.metric_params||{},t.metric||""):t.metric||"";if(t.auc!=null)e=Math.max(0,Math.min(1,(t.auc-.5)/.5))*100,s=t.auc>=.85?this._t("ml.fit_strong",{},"Strong"):t.auc>=.75?this._t("ml.fit_good",{},"Good"):t.auc>=.65?this._t("ml.fit_fair",{},"Fair"):this._t("ml.fit_weak",{},"Weak");else if(t.model_mae!=null&&t.naive_mae!=null&&t.naive_mae>0){const a=Math.max(0,(t.naive_mae-t.model_mae)/t.naive_mae);e=Math.min(1,a)*100,s=a>=.5?this._t("ml.fit_strong",{},"Strong"):a>=.2?this._t("ml.fit_good",{},"Good"):this._t("ml.fit_slight",{},"Slight"),r=this._t("ml.better_than_baseline",{pct:(a*100).toFixed(0),metric:r},`${(a*100).toFixed(0)}% better than the baseline estimate (${r})`)}else return"";const n=e>=70?"var(--success-color,#4caf50)":e>=40?"var(--warning-color,#ff9800)":"var(--secondary-text-color)";return`
${s} ${this._t("ml.fit_word",{},"fit")}
`}_mlTrendBadge(t){if(!t)return"";const s={improving:[this._t("badge.improving",{},"\u2197 improving"),"var(--success-color,#4caf50)",this._t("ml.trend_improving_tip",{},"This model's fit has improved across recent re-checks.")],declining:[this._t("badge.declining",{},"\u2198 declining"),"var(--warning-color,#ff9800)",this._t("ml.trend_declining_tip",{},"This model's fit has slipped across recent re-checks \u2014 reviewing more cycles may help it re-learn.")],steady:[this._t("badge.steady",{},"\u2192 steady"),"var(--secondary-text-color)",this._t("ml.trend_steady_tip",{},"This model's fit has held roughly steady across recent re-checks.")]}[t];return s?` ${s[0]}`:""}_htmlMatchingTuningCard(){const t=this._mlTrainingStatus,e=t&&t.matching;if(!e)return"";const s=e.defaults||{},r=e.tuned||null,n=r&&r.config||null,a=e.active==="tuned"&&n,o=this._busy.has("ml-revert-match"),i=g=>g==null||isNaN(g)?"-":Number(g).toFixed(2),l=[["corr_weight","Shape (correlation)"],["duration_weight","Duration agreement"],["energy_weight","Energy agreement"],["dtw_ensemble_w","DTW derivative blend (DDTW)"]].map(([g,u])=>{const v=s[g],b=a?n[g]:s[g],$=a&&v!=null&&b!=null&&Math.abs(v-b)>1e-9;return`
${this._t("msg.matching_tuning_intro",{},"When learning, WashData also adjusts how much program matching weighs shape versus duration and energy.")}
${c}
${this._t("lbl.emphasis",{},"Emphasis")}
${this._t("lbl.default",{},"Default")}
${this._t("lbl.in_use",{},"In use")}
${l}
${_}
`}_pgOverrideFields(){return[["start_threshold_w","Start Threshold","W","Minimum watts to count as started","detection"],["stop_threshold_w","Stop Threshold","W","Below this, machine counts as off","detection"],["off_delay","Off Delay","s","Seconds of low power before cycle ends","timing"],["min_off_gap","Min Off Gap","s","Gap required to separate two cycles","timing"],["completion_min_seconds","Min Cycle Duration","s","Shortest run that counts as a real cycle","timing"],["start_duration_threshold","Start Duration","s","Seconds above threshold to confirm start","timing"],["end_repeat_count","End Repeat Count","","Low readings in a row before ending","advanced"],["interrupted_min_seconds","Interrupted Min","s","Short cycles flagged as interrupted","advanced"],["anti_wrinkle_enabled","Enable Anti-Wrinkle Detection","","Absorb the tumble pulses after the main phase instead of reading them as new cycles","advanced","bool"],["anti_wrinkle_max_power","Max Anti-Wrinkle Power","W","A pulse above this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_max_duration","Max Anti-Wrinkle Duration","s","A pulse longer than this ends anti-wrinkle and opens a new cycle","advanced"],["anti_wrinkle_exit_power","Anti-Wrinkle Exit Power","W","Power must fall below this between pulses for anti-wrinkle to stay active","advanced"],["anti_wrinkle_idle_timeout","Max Pulse Gap","s","Quiet time allowed between two tumble pulses before anti-wrinkle ends","advanced"],["dishwasher_end_spike_quiet_release","Passive-Dry Quiet Release","s","Dishwasher: quiet seconds after expected duration before the end-of-cycle drain wait is released","advanced"],["smart_termination_duration_ratio","Smart Termination Ratio","","Fraction of the matched program's expected duration a cycle must reach before Smart Termination may end it early; lower it for load- or temperature-dependent machines","advanced"],["profile_match_min_duration_ratio","Min Duration Ratio","","Stage 1: shortest run (vs the profile) still allowed to match","matching"],["profile_match_max_duration_ratio","Max Duration Ratio","","Stage 1: longest run (vs the profile) still allowed to match","matching"],["corr_weight","Correlation Weight","","Stage 2: balance between curve shape (correlation) and power level (MAE); default 0.45","matching"],["keep_min_score","Keep Min Score","","Stage 2: floor score to stay in the race; default 0.1 admits weak matches, later stages pick the best","matching"],["dtw_bandwidth","DTW Bandwidth","","Stage 3: Sakoe-Chiba warp band (0 = DTW off; default 0.2 = 20% of cycle length)","matching"],["dtw_blend","DTW Blend","","Stage 3: 0 = core score only, 1 = DTW score only, 0.5 = equal blend (default)","matching"],["dtw_ensemble_w","DTW Ensemble Weight","","Stage 3 (ensemble mode): weight on scaled-L1 vs derivative DTW; default 0.7 favours level-aware","matching"],["dtw_ddtw_scale","DDTW Scale","","Stage 3: DDTW half-saturation distance; smaller = more shape-sensitive (default 30)","matching"],["dtw_refine_top_n","DTW Refine Top-N","","Stage 3: candidates DTW re-scores; raise to 7-9 if correct profile ranks 4th-5th (default 5)","matching"],["duration_weight","Duration Weight","","Stage 4: how strongly run-length agreement affects the final score (default 0.22)","matching"],["energy_weight","Energy Weight","","Stage 4: how strongly energy agreement affects the final score (default 0.22)","matching"],["duration_scale","Duration Scale","","Stage 4: log-ratio where duration agreement halves; smaller = stricter penalty (default 0.175)","matching"],["energy_scale","Energy Scale","","Stage 4: log-ratio where energy agreement halves; smaller = stricter penalty (default 0.25)","matching"]]}_pgFieldVal(t,e){const s=e||{};if(s[t]!==void 0)return s[t];const r=this._pgEffective||{};if(r[t]!==void 0&&r[t]!==null)return r[t];const n=this._opts||{};if(n[t]!==void 0&&n[t]!==null)return n[t];const a=kt[t]||{};if(a.def!==void 0)return a.def;const o=this._constants&&this._constants.pgMatchDefaults||{};return o[t]!==void 0?o[t]:qt[t]!==void 0?qt[t]:""}async _pgFetchSettings(t,e=!0){try{const s=await this._ws({type:`${y}/get_playground_settings`,entry_id:t,include_suggestions:e});if(!this._isActiveEntry(t))return;this._pgEffective=s.effective||{},this._pgPublishable=Array.isArray(s.publishable)?s.publishable:null,this._pgPresets=Array.isArray(s.presets)?s.presets:[],this._pgPresetLimit=s.preset_limit||0,this._pgPresetSel&&!this._pgPresets.some(r=>r.name===this._pgPresetSel)&&(this._pgPresetSel=""),e&&this._pgApplySuggestions(s)}catch(s){this._pgIsUnknownCmd(s)&&(this._pgNeedsRestart=!0)}}_pgApplySuggestions(t){this._pgSuggClassic=t.classic_suggestions&&typeof t.classic_suggestions=="object"?t.classic_suggestions:{},this._pgSuggMl=t.ml_suggestions&&typeof t.ml_suggestions=="object"?t.ml_suggestions:null,this._pgMlSuggEnabled=!!t.ml_suggestions_enabled}async _pgFetchSuggestions(t){try{const e=await this._ws({type:`${y}/get_playground_settings`,entry_id:t,include_suggestions:!0});if(!this._isActiveEntry(t))return;this._pgApplySuggestions(e),this._tab==="playground"&&this._render()}catch{}}_pgCurrentValues(){const t={};for(const[e]of this._pgOverrideFields()){const s=this._pgFieldVal(e,{});s!==""&&s!==null&&s!==void 0&&(t[e]=s)}this._pgThreshStart!=null&&(t.start_threshold_w=this._pgThreshStart),this._pgThreshStop!=null&&(t.stop_threshold_w=this._pgThreshStop);for(const[e,s]of Object.entries(this._pgParamOverrides||{}))t[e]=s;return t}_pgStagedVal(t){return t==="start_threshold_w"?this._pgThreshStart??void 0:t==="stop_threshold_w"?this._pgThreshStop??void 0:this._pgParamOverrides[t]}_pgSetStaged(t,e){t==="start_threshold_w"?this._pgThreshStart=e:t==="stop_threshold_w"?this._pgThreshStop=e:this._pgParamOverrides[t]=e}_pgClearStaged(t){t==="start_threshold_w"?this._pgThreshStart=null:t==="stop_threshold_w"?this._pgThreshStop=null:delete this._pgParamOverrides[t]}_pgChangedKeys(){const t=this._pgEffective||{},e=[];for(const[s]of this._pgOverrideFields()){const r=this._pgStagedVal(s);if(r==null)continue;const n=t[s];n!=null&&this._pgSameVal(r,n)||e.push(s)}return e}_pgSameVal(t,e){if(typeof t=="boolean"||typeof e=="boolean")return!!t==!!e;const s=parseFloat(t),r=parseFloat(e);return!isNaN(s)&&!isNaN(r)?Math.abs(s-r)<1e-9:String(t)===String(e)}_pgIsPublishable(t){return Array.isArray(this._pgPublishable)?this._pgPublishable.includes(t):!!kt[t]}_htmlPgControlPanel(){const t=this._pgChangedKeys(),e=this._canEdit(),s=!!this._pgEffective,r=t.filter(b=>this._pgIsPublishable(b)),n=s?t.length?`${this._t("msg.pg_n_changed",{n:t.length},t.length+" changed vs live settings")}`:`\u2713 ${this._t("msg.pg_matches_live",{},"Matches live settings")}`:`${this._t("msg.pg_live_unavailable",{},"Live settings unavailable \u2014 showing defaults.")}`,a=``+(this._pgPresets||[]).map(b=>``).join(""),o=!!this._pgPresetSel,i=this._pgPresetLimit>0&&(this._pgPresets||[]).length>=this._pgPresetLimit&&!(this._pgPresets||[]).some(b=>b.name===(this._pgPresetName||"").trim()),l=`
${this._t("msg.pg_canvas_empty2",{},"Pick a cycle above and press Run to simulate it. Then hover to read values, scroll to zoom, and drag to pan.")}
`:"",c=`${i}
${l}
`,_=this._htmlPgStrip(),p=this._pgNeedsRestart?`
\u26A0 ${this._t("msg.pg_restart_note",{},"Restart Home Assistant to enable simulation tools.")}
`:"",g=`${o}${c}${_}
${this._htmlPgParamRows()}
${this._htmlPgAlerts()}${this._htmlPgAnalysis()}
`;return`
${this._t("hdr.playground",{},"Playground")}
${this._t("msg.playground_intro",{},"Explore how settings affect detection on your real cycle data. Nothing here changes live configuration until you explicitly apply it.")}
${d(this._t("lbl.pg_stress_toggle_desc",{},"Simulates the appliance staying at its standby draw after recording ends \u2014 shows if and when WashData stops the cycle."))}
`}_pgAlertLabel(t){return{overrun:this._t("lbl.pg_alert_overrun",{},"Overrun"),underrun:this._t("lbl.pg_alert_underrun",{},"Underrun"),did_not_finish:this._t("lbl.pg_alert_did_not_finish",{},"Did not finish"),false_end:this._t("lbl.pg_alert_false_end",{},"Split into multiple cycles"),unmatched:this._t("lbl.pg_alert_unmatched",{},"Unmatched"),ambiguous:this._t("lbl.pg_alert_ambiguous",{},"Ambiguous match"),energy_anomaly:this._t("lbl.pg_alert_energy",{},"Energy anomaly"),timeout_end:this._t("lbl.pg_alert_timeout_end",{},"Ended by timeout, not prediction"),would_run_indefinitely:this._t("lbl.pg_alert_indefinite",{},"Would run indefinitely"),stress_terminated:this._t("lbl.pg_alert_stress_ok",{},"Idle termination: cycle stopped"),stress_above_threshold:this._t("lbl.pg_alert_stress_warn",{},"Idle draw above stop threshold"),stress_hit_cap:this._t("lbl.pg_alert_stress_cap",{},"Hit safety cap")}[t]||t}_htmlPgHistoryMode(){const t=this._busy.has("pg-history"),e=this._pgHistory,s=Object.keys(this._pgParamOverrides||{}).length>0||this._pgThreshStart!=null||this._pgThreshStop!=null,r=`
${this._t("lbl.last",{},"Last")}${this._t("lbl.cycles_lc",{},"cycles")}
${s?`\u2699 ${this._t("msg.pg_override_active",{},"Using your edited settings vs. current")}`:""}
`,n=t?this._htmlPgBatchBar():"",a=`
${this._t("msg.pg_history_intro2",{},"Replay your recent cycles through the real detector and matcher with the settings above. Click any row to load that cycle in the graph; edit a setting to see a before/after comparison.")}
${this._t("msg.process_history_hint",{},"Re-run matching on all stored cycles, refresh tuning suggestions, retrain the ML models (if enabled), and recompute cycle health. Run this after a batch of reviews.")}
${this._t("msg.export_description",{},"Choose exactly which profiles, cycles, settings and more to export to JSON, or analyze a file and import only the parts you want.")}
${this._t("hdr.import_power_history",{},"Import power history")}
${this._t("msg.import_history_description",{},"Already had a smart plug before WashData? Upload a history export of its power sensor, or read it straight from Home Assistant, and the normal detection runs over it so past cycles turn up in your Cycles list ready to name.")}
`:`
${this._t("msg.maintenance_requires_access",{},"Maintenance and export/import require full access.")}
${this._t("msg.prefs_personal",{},"These apply to your Home Assistant account only.")}
${this._t("hdr.display",{},"Display")}
${Math.round((t.font_scale||1)*100)}%
${this._t("msg.font_size_hint",{},"Make everything in this panel larger or smaller. Applies to your account on this device.")}
${this._t("hdr.status_graph",{},"Status Graph")}
${Et(`id="wd-pref-expected" ${t.show_expected!==!1?"checked":""}`,this._t("lbl.show_expected",{},"Show expected curve overlay (matched profile, orange)"))}
${Et(`id="wd-pref-raw" ${t.show_raw?"checked":""}`,this._t("lbl.show_raw",{},"Show raw socket toggle in live power graph"))}
${Et(`id="wd-pref-debug" ${t.show_debug?"checked":""}`,this._t("lbl.show_debug",{},"Show live match debug card on the Status page (confidence, ambiguity, top candidates)"))}
${Et(`id="wd-rbac-enabled" ${t.enabled?"checked":""}`,this._t("lbl.enable_access_control",{},"Enable per-user access control"),"",this._t("msg.rbac_hint",{},"When off, every Home Assistant user has full access (the default). Administrators always have full access and can manage everyone."))}
${this._t("msg.store_declare_appliance",{},"Tell WashData which appliance you own and this tab shows the setups other people have shared for it. You can also type a brand above to look around.")}
`;const r=`
${this._t("store.no_results",{},"No matching appliances found. Try a different search.")}
${this._t("msg.store_sibling_hint",{},"Nothing shared for your exact model? A closely-related model from the same brand is usually a good starting point.")}
${this._t("msg.store_download_device_intro",{},"Adopt every shared program and its reference cycles onto your device. Your own recorded cycles and stats are not affected.")}
${Nt(`data-action="store-toggle-dl-settings" ${this._dlSettings?"checked":""} ${n?"disabled":""}`,this._t("lbl.adopt_settings",{},"Also adopt settings"),it(this._t("msg.adopt_settings_hint",{},"Overwrite this device's detection & matching thresholds with the shared ones. Your notifications, entities and energy price are never changed.")))}
${this._t("store.connected_as",{name:d(e.name||e.uid||"")},`Connected as ${d(e.name||e.uid||"")}`)}
`:`
${this._t("store.not_connected",{},"Not connected. Connect a GitHub account to confirm appliances and share your own cycles.")}
`:"";return`
${this._t("hdr.online_account",{},"Community Store & online features")}
${this._t("msg.online_intro_global",{},"Browse and share reference recordings with other WashData users, and confirm appliance entries. One connection applies to your whole WashData integration; appliance brand and model are set per device under Basic. All online features are opt-in and off by default.")}
${this._t("msg.refresh_catalog_hint",{},"The community brand and appliance lists are cached to keep the shared store within its daily budget. Refresh to pick up entries added or approved by others.")}
${this._t("msg.manual_duration_ref_hint",{},"Only used when no reference cycle is selected \u2014 a reference cycle sets the duration from its own length.")}
${this._t("msg.edit_builtin_phase",{},"This is a built-in phase. Saving creates a custom override \u2014 the original is preserved and can be restored by deleting the override.")}
${this._t("modal.store_share",{},"Share to community store")}
${this._t("msg.store_share_intro",{},"Upload this reference cycle so others with the same appliance can use it. It is reviewed before appearing publicly.")}
${d(u.program)}${this._t("msg.share_profile_no_cycles",{},"No reference cycles \u2014 mark a cycle as \u2B50 in the Cycles tab to include this profile")}
${this._t("msg.share_device_none",{},"No shareable cycles yet. Mark a recorded or hand-picked cycle as a reference cycle (\u2B50) in the Cycles tab first.")}
`;const l=d((this._opts.store_brand||"").trim()),c=d((this._opts.store_model||"").trim()),_=``,p=`${this._t("msg.share_guidelines_title",{},"Before you share")}
${this._t("msg.share_guideline_naming",{},"Name each profile exactly as shown on the appliance dial or display (e.g. 'Cotton 40', 'Eco 60').")}
${this._t("msg.share_guideline_quality",{},"Only share cycles that completed normally -- no mid-cycle interruptions, door-open events, or power blips.")}
${this._t("msg.share_guideline_review",{},"Your upload starts as pending and appears publicly once enough community members confirm it.")}
`,g=o?``:"";return`
${this._t("modal.store_share_device",{},"Share this device")}
${this._t("msg.store_share_device_intro",{brand:l,model:c},`Upload ${l} ${c} with the reference cycles you select. Others with the same appliance can adopt your programs. Entries are reviewed before appearing publicly.`)}
${this._t("msg.export_select_intro",{},"Tick exactly what to include. Selecting profiles without their cycles still exports a matchable program (its learned shape travels along).")}
${this._t("msg.import_analyze_hint",{},"Load an exported file (or paste its JSON). WashData analyzes it and shows exactly what can be imported before anything changes.")}
${this._t("msg.device_type_mismatch_warn",{src:a,local:o},"This export is from a different appliance type ("+a+" vs "+o+"). Programs and cycles can still be imported as reference data, but device-specific settings and real-history import are disabled.")}
${t.mode==="replace"?this._t("msg.replace_warn",{},"Each ticked category is wiped and replaced from the file. Unticked categories are left untouched."):this._t("msg.merge_hint",{},"Imported items are added; nothing local is lost. Name clashes are resolved below.")}
`,_=r.real_history_allowed!==!1,p=`
${t.cycleDest==="real_history"?this._t("msg.dest_real_history_hint",{},"Imported cycles count as this device's own history and feed energy/usage stats. Use for moving one appliance to a new install."):this._t("msg.dest_reference_hint",{},"Imported cycles only improve program matching and never affect usage/energy statistics.")}
`}_histSkipReason(t){return{idle:this._t("lbl.hist_skip_idle",{},"nothing running"),sparse:this._t("lbl.hist_skip_sparse",{},"readings too far apart"),too_few_samples:this._t("lbl.hist_skip_short",{},"too few readings"),too_long:this._t("lbl.hist_skip_long",{},"no break long enough to split on")}[t]||t||""}_histSegReason(t){return{shorter_than_minimum:this._t("lbl.hist_reason_short",{},"shorter than this appliance's shortest real cycle"),no_clean_end:this._t("lbl.hist_reason_no_end",{},"never ended cleanly")}[t]||""}_htmlHistoryImportModal(t){const e=`
${this._t("modal.history_import",{},"Import power history")}
${this._t("msg.hist_input_hint",{},"Upload a CSV downloaded from the History panel (entity, state, last changed), or let WashData read the sensor's history directly. Detection then runs over it exactly as it does live, and you choose which of the cycles it finds to keep.")}
${this._t("lbl.hist_since",{},"Since")}
${this._t("msg.hist_recorder_hint",{},"Reads from the date you pick up to now. Home Assistant keeps detailed history for 10 days by default and only hourly averages after that, which are too coarse to detect cycles from - pick a date further back only if your recorder is set to keep more.")}
${this._t("msg.hist_scanning",{},"Replaying your history through the detector. This runs in the background - you can close this dialog and come back to it.")}
`}if(t.step==="done"){const $=t.done||{},k=[this._t("msg.hist_imported_count",{n:$.imported||0},`${$.imported||0} cycles imported.`),$.duplicates?this._t("msg.hist_duplicates",{n:$.duplicates},`${$.duplicates} were already imported and were skipped.`):"",$.capped?this._t("msg.hist_capped",{},"The per-device limit for imported cycles was reached; the rest were not stored."):""].filter(Boolean);return`${e}
${k.map(S=>`
${d(S)}
`).join("")}
${this._t("msg.hist_next_step",{},"They are in your Cycles list, tagged as imported history. Open one and use Label to name the program it belongs to.")}
`}const r=t.result||{},n=r.segments||[],a=r.parse||{},o=this._busy.has("hist-apply"),i=t.accept||new Set,l=[];a.rows_total&&l.push(this._t("msg.hist_rows_read",{n:a.rows_total},`${a.rows_total} readings read`)),a.first&&a.last&&l.push(`${st(a.first)} \u2013 ${st(a.last)}`),a.breaks&&l.push(this._t("msg.hist_breaks",{n:a.breaks},`${a.breaks} gaps where the sensor was unavailable`)),a.rows_other_entity&&l.push(this._t("msg.hist_other_entity",{n:a.rows_other_entity},`${a.rows_other_entity} readings for other entities ignored`)),a.entity_substituted_from&&l.push(this._t("msg.hist_entity_substituted",{used:a.entity_id||"?",wanted:a.entity_substituted_from},`read ${a.entity_id||"?"} (this device is configured for ${a.entity_substituted_from})`));const c=r.skipped||[],_={};c.forEach($=>{_[$.reason]=(_[$.reason]||0)+1});const p=Object.entries(_).map(([$,k])=>`${k} \xD7 ${this._histSkipReason($)}`).join(", "),g=r.settings||{},u=g.min_power!=null?this._t("msg.hist_settings_used",{w:g.min_power,s:g.off_delay},`Detected using this device's current settings (minimum power ${g.min_power} W, off delay ${g.off_delay} s).`):"";if(!n.length)return`${e}
${this._t("msg.hist_none_found",{},"No cycles could be detected in that history.")}
${this._t("msg.hist_found",{n:n.length},`Found ${n.length} cycles. Untick anything that does not look like a real run - nothing is stored until you import.`)}
\u{1F4E5} ${a?this._t("msg.imported_history_readonly",{},"Detected in imported power history. It shapes program matching but is not counted in your statistics, and cannot be trimmed or split. Label it to name the program."):this._t("msg.imported_readonly",{},"Imported from the community store. Shown for reference and matching. It is not counted in your stats and cannot be edited.")}
${this._t("msg.unsure_detected_prefix",{},"WashData is unsure it detected")} ${d(S)}${k.confidence!=null?` (${this._t("lbl.confidence",{},"confidence").toLowerCase()} ${(k.confidence*100).toFixed(0)}%)`:""}. ${this._t("msg.feedback_prompt",{},"Confirm it was right, correct the program, or ignore.")} ${this._t("msg.feedback_relabel_hint",{},"Re-labelling this cycle resolves it too.")}
${this._t("msg.split_intro",{},"Click the graph to add or remove a split point, or auto-detect by idle gaps. Each resulting segment can get its own profile.")}
`}else if(t.mode==="review"){const E=l&&l.ml_review||{},M=this._busy.has("cyc-review-save"),H=(f,m)=>``,Q=[["late_start",this._t("tag.late_start",{},"Late start")],["early_end",this._t("tag.early_end",{},"Early end")],["merged",this._t("tag.merged",{},"Merged cycles")],["split",this._t("tag.split",{},"Split cycle")],["noise",this._t("tag.noise",{},"Noise")],["wrong_profile",this._t("tag.wrong_profile",{},"Wrong profile")],["sensor_gap",this._t("tag.sensor_gap",{},"Sensor gap")]].map(([f,m])=>``).join(""),J=E.reviewed_at?`${this._t("lbl.reviewed_on",{date:new Date(E.reviewed_at).toLocaleDateString()},`reviewed ${new Date(E.reviewed_at).toLocaleDateString()}`)}`:"",G=it(this._t("msg.review_profile_tip",{},"The program this cycle is labelled as. If the auto-detected program was wrong, correct it here - labelling teaches matching for future cycles.")),X=it(this._t("msg.review_quality_tip",{},"How clean this cycle is. Good = a textbook example of this program; Bad = detected but noisy or atypical; Unusable = mis-detected (merged, truncated or spurious). Drives the health score and which cycles are allowed to train the model.")),ct=it(this._t("msg.review_recorded_tip",{},'Mark this as a hand-picked reference cycle for its program - the same role as a manually recorded cycle. Reference cycles are always kept, seed the matching template, and are never dropped by cleanup. (This is the "golden"/recorded flag; both are the same thing.)')),U=it(this._t("msg.review_tags_tip",{},"Optional flags describing what went wrong with this cycle, so training and cleanup can account for it.")),at=it(this._t("msg.review_notes_tip",{},"Free-text notes for your own reference. Not used by matching or training."));R=`
${C}
${this._t("msg.review_confirm_help",{},"Confirm whether this cycle was detected correctly. Your reviews train the model on your machine - the more cycles you confirm, the better matching and health scoring get. A quick Good/Bad is enough.")}
${J}
${this._t("lbl.compare_profiles",{},"Compare with profiles")}${it(this._t("msg.compare_profiles_tip",{},"Overlay other profile envelopes on the chart above to see which one best fits this cycle."))}
${(this._profiles||[]).map(f=>{const m=(t.overlays||[]).includes(f.name),x=m?``:"";return``}).join("")||`${this._t("msg.no_profiles_compare",{},"No profiles to compare.")}`}
\u26A0 ${this._t("msg.artifact_header",{n:W.length},`${W.length} anomal${W.length>1?"ies":"y"} detected during this cycle`)}
${E}
${this._t("msg.artifact_footer",{},"Highlighted on the graph above. These are transient artifacts (e.g. the door opened mid-cycle), not necessarily problems.")}
`}let O="";const D=t.mode==="view"||t.mode==="review"?e.restart_gaps||[]:[];if(D.length){const E=D.map(M=>{const H=Math.round((M.gap_seconds||0)/60),q=H>=1?`${H}m`:`${Math.round(M.gap_seconds||0)}s`,Q=M.match_confidence!=null?` \xB7 ${this._t("lbl.pct_match_confidence",{pct:Math.round(M.match_confidence*100)},`${Math.round(M.match_confidence*100)}% match confidence`)}`:"",J=M.profile?` (${d(M.profile)})`:"";return`
${this._t("msg.samples_decimated",{shown:E,total:M},`Showing ${E} of ${M} samples (thinned for display; peaks kept). A wide gap here is thinning, not missing data.`)}
${p.health_status==="poor"?this._t("health.poor",{},"\u26A0 Poor match fit"):p.health_status==="fair"?this._t("health.fair",{},"Fair match fit"):this._t("health.good",{},"\u2713 Good match fit")}${this._t("stat.score",{pct:S},`score ${S}%`)}${C}${R}
${p.health_status==="poor"?`${this._t("msg.profile_poor_health_detail",{},"Cycles assigned to this profile have inconsistent shapes or low confidence. Consider rebuilding the envelope or reviewing labelled cycles.")}`:""}
${this._t("msg.shape_drift_advisory",{},"\u26A0 Shape drifting")}${d(b)}${this._t("msg.shape_drift_detail",{},"The power pattern for this profile has shifted over time \u2014 possible appliance wear or maintenance needed (e.g. descaling, filter cleaning).")}
${this._t("msg.manual_duration_hint",{},"The profile's average/expected cycle length, used for time-remaining estimates. Edit to set it; leaving it unchanged keeps the current value.")}
${this._t("msg.compare_selected_cycles",{},"Selected cycles (solid) \u2014 show / hide")}
${n}
${this._t("msg.compare_overlay_profiles",{},"Overlay profiles (faint)")}${it(this._t("msg.compare_overlay_tip",{},"Overlay learned profile envelopes to see which program each cycle resembles."))}