@@ -78,4 +133,4 @@ const ze={attribute:!0,type:String,converter:O,reflect:!1,hasChanged:R},ke=(e=ze
${i("card.no_device")}
- `}};Ht.styles=C("\n :host {\n --span-accent: var(--primary-color, #4dd9af);\n }\n\n ha-card {\n padding: 24px;\n background: var(--card-background-color, #1c1c1c);\n color: var(--primary-text-color, #e0e0e0);\n border-radius: var(--ha-card-border-radius, 12px);\n border: var(--ha-card-border-width, 1px) solid var(--ha-card-border-color, var(--divider-color, #333));\n box-shadow: var(--ha-card-box-shadow, none);\n }\n\n .panel-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px 16px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .header-left { flex: 1 1 300px; min-width: 0; }\n .header-center { flex: 0 0 auto; }\n .header-right { flex: 0 1 auto; min-width: 0; }\n\n .panel-identity {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px 12px;\n margin-bottom: 12px;\n }\n\n .panel-title {\n font-size: 1.8em;\n font-weight: 700;\n margin: 0;\n color: var(--primary-text-color, #fff);\n }\n\n .panel-serial {\n font-size: 0.85em;\n color: var(--secondary-text-color, #999);\n font-family: monospace;\n }\n\n .panel-stats {\n display: flex;\n flex-wrap: wrap;\n gap: 16px 32px;\n }\n\n .stat { display: flex; flex-direction: column; }\n .stat-label { font-size: 0.8em; color: var(--secondary-text-color, #999); margin-bottom: 2px; }\n .stat-row { display: flex; align-items: baseline; gap: 2px; }\n .stat-value { font-size: 1.5em; font-weight: 700; color: var(--primary-text-color, #fff); }\n .stat-unit { font-size: 0.7em; font-weight: 400; color: var(--secondary-text-color, #999); }\n\n .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; padding-top: 8px; }\n .header-right-top { display: flex; gap: 20px; align-items: center; }\n .meta-item { font-size: 0.8em; color: var(--secondary-text-color, #999); }\n\n .shedding-legend { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }\n .shedding-legend-item { display: inline-flex; align-items: center; gap: 3px; }\n .shedding-legend-item ha-icon { --mdc-icon-size: 16px; }\n .shedding-legend-secondary { --mdc-icon-size: 12px; opacity: 0.8; }\n .shedding-legend-text { font-size: 9px; font-weight: 600; }\n .shedding-legend-label { font-size: 0.7em; color: var(--secondary-text-color, #999); }\n\n .panel-gear {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color);\n opacity: 0.6;\n padding: 4px;\n margin-left: 8px;\n vertical-align: middle;\n }\n .panel-gear:hover { opacity: 1; }\n .header-center {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding-top: 8px;\n }\n .panel-identity .panel-gear {\n margin-left: 0;\n }\n .slide-confirm {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 160px;\n height: 28px;\n border-radius: 14px;\n background: color-mix(in srgb, var(--primary-color, #4dd9af) 20%, var(--secondary-background-color, #333));\n vertical-align: middle;\n overflow: hidden;\n user-select: none;\n touch-action: none;\n }\n .slide-confirm-text {\n position: absolute;\n width: 100%;\n text-align: center;\n font-size: 0.65em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n pointer-events: none;\n z-index: 0;\n }\n .slide-confirm-knob {\n position: absolute;\n left: 2px;\n top: 2px;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--secondary-text-color, #666);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: grab;\n z-index: 1;\n transition: none;\n }\n .slide-confirm-knob ha-icon {\n --mdc-icon-size: 14px;\n color: var(--card-background-color, #1c1c1c);\n }\n .slide-confirm-knob.snapping {\n transition: left 0.25s ease;\n }\n .slide-confirm.confirmed {\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n }\n .slide-confirm.confirmed .slide-confirm-text {\n color: var(--state-active-color, var(--span-accent));\n }\n .slide-confirm.confirmed .slide-confirm-knob {\n background: var(--state-active-color, var(--span-accent));\n }\n .switches-disabled .toggle-pill {\n opacity: 0.3;\n pointer-events: none;\n }\n .unit-toggle {\n display: inline-flex;\n background: var(--secondary-background-color, #333);\n border-radius: 6px;\n overflow: hidden;\n margin-left: 8px;\n }\n .unit-btn {\n padding: 4px 10px;\n border: none;\n background: none;\n color: var(--secondary-text-color);\n font-size: 0.75em;\n font-weight: 600;\n cursor: pointer;\n }\n .unit-btn.unit-active {\n background: var(--primary-color, #4dd9af);\n color: var(--text-primary-color, #000);\n }\n\n .monitoring-summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 16px;\n font-size: 0.8em;\n background: rgba(76, 175, 80, 0.1);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n }\n .monitoring-active { color: #4caf50; }\n .monitoring-counts { display: flex; gap: 12px; }\n .count-warning { color: #ff9800; }\n .count-alert { color: #f44336; }\n .count-overrides { color: var(--secondary-text-color); }\n\n .panel-grid {\n display: grid;\n grid-template-columns: 28px 1fr 1fr 28px;\n gap: 8px;\n align-items: stretch;\n }\n\n .tab-label {\n display: flex;\n align-items: center;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n user-select: none;\n }\n .tab-left { justify-content: flex-start; }\n .tab-right { justify-content: flex-end; }\n\n .circuit-slot {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px 20px;\n min-height: 140px;\n transition: opacity 0.3s;\n position: relative;\n overflow: hidden;\n }\n\n .circuit-col-span { min-height: 280px; }\n .circuit-row-span { border-left: 3px solid var(--span-accent); }\n .circuit-off .circuit-name,\n .circuit-off .breaker-badge,\n .circuit-off .power-value,\n .circuit-off .chart-container { opacity: 0.35; }\n .circuit-off .toggle-pill,\n .circuit-off .gear-icon { opacity: 1; }\n\n .circuit-empty {\n opacity: 0.2;\n min-height: 60px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-style: dashed;\n }\n .empty-label { color: var(--secondary-text-color, #999); font-size: 0.85em; }\n\n .circuit-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 6px;\n gap: 8px;\n }\n\n .circuit-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }\n\n .breaker-badge {\n background: color-mix(in srgb, var(--span-accent) 15%, transparent);\n color: var(--span-accent);\n font-size: 0.7em;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 4px;\n white-space: nowrap;\n border: 1px solid color-mix(in srgb, var(--span-accent) 25%, transparent);\n flex-shrink: 0;\n }\n\n .circuit-name {\n font-size: 0.9em;\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--primary-text-color, #e0e0e0);\n }\n\n .circuit-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n\n .power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .power-value strong { font-weight: 700; font-size: 1.1em; }\n .power-unit { font-size: 0.8em; font-weight: 400; color: var(--secondary-text-color, #999); margin-left: 1px; }\n .circuit-producer .power-value strong { color: var(--info-color, #4fc3f7); }\n\n .toggle-pill {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 4px;\n border-radius: 10px;\n cursor: pointer;\n font-size: 0.65em;\n font-weight: 600;\n transition: background 0.2s;\n user-select: none;\n min-width: 40px;\n }\n .toggle-on {\n padding-left: 6px;\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n color: var(--state-active-color, var(--span-accent));\n }\n .toggle-off {\n padding-right: 6px;\n background: color-mix(in srgb, var(--secondary-text-color) 15%, transparent);\n color: var(--secondary-text-color, #999);\n }\n .toggle-knob {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n transition: background 0.2s, margin 0.2s;\n }\n .toggle-on .toggle-knob {\n background: var(--state-active-color, var(--span-accent));\n margin-left: auto;\n }\n .toggle-off .toggle-knob {\n background: var(--secondary-text-color, #999);\n margin-right: auto;\n order: -1;\n }\n\n .circuit-status {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-top: 4px;\n padding: 0 4px;\n }\n .shedding-icon { opacity: 0.8; cursor: default; }\n .shedding-composite {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n }\n .shedding-icon-secondary { opacity: 0.8; }\n .shedding-label {\n font-size: 10px;\n font-weight: 600;\n opacity: 0.8;\n }\n .gear-icon {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n opacity: 0.6;\n transition: opacity 0.2s;\n margin-left: auto;\n }\n .gear-icon:hover { opacity: 1; }\n .utilization {\n font-size: 0.75em;\n font-weight: 600;\n }\n .utilization-normal { color: #4caf50; }\n .utilization-warning { color: #ff9800; }\n .utilization-alert { color: #f44336; }\n .circuit-alert {\n border-color: #f44336 !important;\n box-shadow: 0 0 8px rgba(244, 67, 54, 0.3);\n }\n .circuit-custom-monitoring {\n border-left: 3px solid #ff9800;\n }\n\n .chart-container {\n width: 100%;\n aspect-ratio: 4 / 1;\n margin-top: 4px;\n overflow: hidden;\n min-width: 0;\n }\n\n .sub-devices {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 12px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .sub-device {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px;\n }\n .sub-device-bess,\n .sub-device-full {\n grid-column: 1 / -1;\n }\n\n .sub-device-header { display: flex; gap: 10px; align-items: baseline; margin-bottom: 8px; }\n .sub-device-type { font-size: 0.7em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--span-accent); }\n .sub-device-name { font-size: 0.85em; color: var(--secondary-text-color, #999); flex: 1; }\n .sub-power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .sub-power-value strong { font-weight: 700; font-size: 1.1em; }\n .sub-device .chart-container { margin-bottom: 8px; aspect-ratio: auto; }\n\n .bess-charts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(0, 1fr));\n gap: 12px;\n margin-bottom: 10px;\n }\n .bess-chart-col { min-width: 0; }\n .bess-chart-title {\n font-size: 0.75em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--secondary-text-color, #999);\n margin-bottom: 4px;\n }\n .bess-chart-col .chart-container { aspect-ratio: auto; }\n .sub-entity { display: flex; gap: 6px; padding: 3px 0; font-size: 0.85em; }\n .sub-entity-name { color: var(--secondary-text-color, #999); }\n .sub-entity-value { font-weight: 500; color: var(--primary-text-color, #e0e0e0); }\n\n /* ── Shared tab bar ────────────────────────────────────── */\n\n .shared-tab-bar {\n display: flex;\n gap: 0;\n margin-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .shared-tab {\n padding: 8px 16px;\n cursor: pointer;\n font-size: 0.9em;\n font-weight: 500;\n color: var(--primary-text-color);\n opacity: 0.6;\n border: none;\n border-bottom: 2px solid transparent;\n background: none;\n transition: opacity 0.15s;\n }\n\n .shared-tab:hover {\n opacity: 0.85;\n }\n\n .shared-tab.active {\n opacity: 1;\n border-bottom-color: var(--span-accent);\n }\n\n /* ── List view search ──────────────────────────────────── */\n\n .list-search-container {\n margin-bottom: 12px;\n position: relative;\n }\n\n .list-search {\n width: 100%;\n padding: 8px 36px 8px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--secondary-background-color, #2a2a2a);\n color: var(--primary-text-color);\n font-size: 0.9em;\n box-sizing: border-box;\n outline: none;\n }\n\n .list-search:focus {\n border-color: var(--span-accent);\n }\n\n .list-search-clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 2px;\n display: flex;\n align-items: center;\n opacity: 0.7;\n }\n\n .list-search-clear:hover {\n opacity: 1;\n }\n\n .list-unit-toggle {\n display: inline-flex;\n margin-bottom: 12px;\n }\n\n /* ── List rows ─────────────────────────────────────────── */\n\n .list-view {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .list-row {\n display: flex;\n align-items: center;\n padding: 12px 16px;\n gap: 10px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n }\n\n .list-row:hover {\n background: var(--secondary-background-color, #2a2a2a);\n }\n\n .list-row.circuit-off {\n opacity: 0.5;\n }\n\n .list-row.list-row-expanded {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-bottom-color: transparent;\n }\n\n .list-circuit-name {\n flex: 1;\n color: var(--primary-text-color);\n font-size: 0.9em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .list-status-badge {\n font-size: 0.75em;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n flex-shrink: 0;\n }\n\n .list-status-on {\n color: #4dd9af;\n }\n\n .list-status-off {\n color: #f44336;\n }\n\n .list-power-value {\n font-size: 0.9em;\n font-weight: 600;\n min-width: 70px;\n text-align: right;\n flex-shrink: 0;\n }\n\n .list-expand-toggle {\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 4px;\n transition: transform 0.2s;\n display: flex;\n align-items: center;\n flex-shrink: 0;\n }\n\n .list-expand-toggle.expanded {\n transform: rotate(180deg);\n }\n\n /* ── Expanded circuit content ──────────────────────────── */\n\n .list-expanded-content {\n padding: 12px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n border-radius: 0 0 8px 8px;\n margin-top: -6px;\n margin-bottom: 2px;\n }\n\n .list-expanded-content .circuit-slot {\n border: none;\n margin: 0;\n background: none;\n }\n\n /* ── Area headers ──────────────────────────────────────── */\n\n .area-header {\n padding: 16px 12px 6px;\n font-weight: 600;\n font-size: 0.85em;\n color: var(--secondary-text-color);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n /* ── No results ────────────────────────────────────────── */\n\n .list-no-results {\n padding: 24px;\n text-align: center;\n color: var(--secondary-text-color);\n }\n\n"),v([Ae({attribute:!1})],Ht.prototype,"hass",void 0),v([Me()],Ht.prototype,"_config",void 0),v([Me()],Ht.prototype,"_discovered",void 0),v([Me()],Ht.prototype,"_discovering",void 0),v([Me()],Ht.prototype,"_discoveryError",void 0),v([Me()],Ht.prototype,"_topology",void 0),v([Me()],Ht.prototype,"_activeTab",void 0),Ht=v([(e=>(t,n)=>{void 0!==n?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)})("span-panel-card")],Ht);class Ot extends HTMLElement{constructor(){super(...arguments),this._config={},this._hass=null,this._panels=null,this._availableRoles=null,this._built=!1,this._panelSelect=null,this._daysInput=null,this._hoursInput=null,this._minsInput=null,this._metricSelect=null,this._checkboxes={},this._entityContainers={},this._tabStyleSelect=null}setConfig(e){this._config={...e},this._updateControls()}set hass(e){this._hass=e,this._panels?this._built||this._buildEditor():this._discoverPanels()}async _discoverPanels(){if(!this._hass)return;const e=await this._hass.callWS({type:"config/device_registry/list"});this._panels=e.filter(e=>(e.identifiers??[]).some(e=>e[0]===r)&&!e.via_device_id).map(e=>{const t=(e.identifiers??[]).find(e=>e[0]===r)?.[1]??"",n=e.name_by_user??e.name??i("editor.panel_label");return{device_id:e.id,label:`${n} (${t})`}}),this._buildEditor()}_buildEditor(){this.innerHTML="",this._built=!0;const e=document.createElement("div");e.style.padding="16px";const t="\n width: 100%;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--card-background-color, var(--secondary-background-color, #1c1c1c));\n color: var(--primary-text-color, #e0e0e0);\n font-size: 1em;\n cursor: pointer;\n appearance: auto;\n box-sizing: border-box;\n ",n="display: block; font-weight: 500; margin-bottom: 8px; color: var(--primary-text-color);",i="margin-bottom: 16px;";this._buildPanelSelector(e,t,n,i),this._buildTimeWindow(e,t,n,i),this._buildMetricSelector(e,t,n,i),this._buildTabStyleSelector(e,t,n,i),this._buildSectionCheckboxes(e,n,i),this.appendChild(e),this._populateMetricSelect(),this._config.device_id&&this._discoverAvailableRoles(this._config.device_id)}_buildPanelSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const a=document.createElement("label");a.textContent=i("editor.panel_label"),a.style.cssText=n;const r=document.createElement("select");r.style.cssText=t;const c=document.createElement("option");if(c.value="",c.textContent=i("editor.select_panel"),r.appendChild(c),this._panels)for(const e of this._panels){const t=document.createElement("option");t.value=e.device_id,t.textContent=e.label,e.device_id===this._config.device_id&&(t.selected=!0),r.appendChild(t)}r.addEventListener("change",()=>{this._config={...this._config,device_id:r.value},this._fireConfigChanged(),this._discoverAvailableRoles(r.value)}),o.appendChild(a),o.appendChild(r),e.appendChild(o),this._panelSelect=r}_buildTimeWindow(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const a=document.createElement("label");a.textContent=i("editor.chart_window"),a.style.cssText=n;const r=document.createElement("div");r.style.cssText="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;";const c=t+"width: 70px; cursor: text;",l=(e,t,n,i)=>{const s=document.createElement("div");s.style.cssText="display: flex; align-items: center; gap: 6px;";const o=document.createElement("input");o.type="number",o.min=t,o.max=n,o.value=String(e),o.style.cssText=c;const a=document.createElement("span");return a.textContent=i,a.style.cssText="font-size: 0.9em; color: var(--secondary-text-color);",s.appendChild(o),s.appendChild(a),{wrap:s,input:o}},d=parseInt(String(this._config.history_days))||0,h=parseInt(String(this._config.history_hours))||0,p=parseInt(String(this._config.history_minutes))||0,u=l(d,"0","30",i("editor.days")),g=l(h,"0","23",i("editor.hours")),_=l(p,"0","59",i("editor.minutes")),f=()=>{this._config={...this._config,history_days:parseInt(u.input.value)||0,history_hours:parseInt(g.input.value)||0,history_minutes:parseInt(_.input.value)||0},this._fireConfigChanged()};u.input.addEventListener("change",f),g.input.addEventListener("change",f),_.input.addEventListener("change",f),r.appendChild(u.wrap),r.appendChild(g.wrap),r.appendChild(_.wrap),o.appendChild(a),o.appendChild(r),e.appendChild(o),this._daysInput=u.input,this._hoursInput=g.input,this._minsInput=_.input}_buildMetricSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const a=document.createElement("label");a.textContent=i("editor.chart_metric"),a.style.cssText=n;const r=document.createElement("select");r.style.cssText=t,r.addEventListener("change",()=>{this._config={...this._config,chart_metric:r.value},this._fireConfigChanged()}),o.appendChild(a),o.appendChild(r),e.appendChild(o),this._metricSelect=r}_buildTabStyleSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const a=document.createElement("label");a.textContent=i("editor.tab_style"),a.style.cssText=n;const r=document.createElement("select");r.style.cssText=t;const c=[{value:"text",text:i("editor.tab_style_text")},{value:"icon",text:i("editor.tab_style_icon")}];for(const e of c){const t=document.createElement("option");t.value=e.value,t.textContent=e.text,e.value===(this._config.tab_style??"text")&&(t.selected=!0),r.appendChild(t)}r.addEventListener("change",()=>{this._config={...this._config,tab_style:r.value},this._fireConfigChanged()}),o.appendChild(a),o.appendChild(r),e.appendChild(o),this._tabStyleSelect=r}_buildSectionCheckboxes(e,t,n){const s=document.createElement("div");s.style.cssText=n;const o=document.createElement("label");o.textContent=i("editor.visible_sections"),o.style.cssText=t,s.appendChild(o);const a=[{key:"show_panel",label:i("editor.panel_circuits"),subDeviceType:null},{key:"show_battery",label:i("editor.battery_bess"),subDeviceType:"bess"},{key:"show_evse",label:i("editor.ev_charger_evse"),subDeviceType:"evse"}];this._checkboxes={},this._entityContainers={};for(const e of a){const t=document.createElement("div");t.style.cssText="display: flex; align-items: center; gap: 8px; margin-bottom: 6px; cursor: pointer;";const n=document.createElement("input");n.type="checkbox",n.checked=!1!==this._config[e.key],n.style.cssText="width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary-color);";const i=document.createElement("span");i.textContent=e.label,i.style.cssText="font-size: 0.9em; color: var(--primary-text-color); cursor: pointer;",t.appendChild(n),t.appendChild(i),s.appendChild(t),this._checkboxes[e.key]=n;let o=null;e.subDeviceType&&(o=document.createElement("div"),o.style.cssText="padding-left: 26px;",o.style.display=n.checked?"block":"none",s.appendChild(o),this._entityContainers[e.subDeviceType]=o),n.addEventListener("change",()=>{this._config={...this._config,[e.key]:n.checked},o&&(o.style.display=n.checked?"block":"none"),this._fireConfigChanged()})}e.appendChild(s)}_isChartEntity(e,t,n){const i=(t.original_name??"").toLowerCase(),s=t.unique_id??"";if("power"===i||"battery power"===i||s.endsWith("_power"))return!0;if("bess"===n){if("battery level"===i||"battery percentage"===i||s.endsWith("_battery_level")||s.endsWith("_battery_percentage"))return!0;if("state of energy"===i||s.endsWith("_soe_kwh"))return!0;if("nameplate capacity"===i||s.endsWith("_nameplate_capacity"))return!0}return!1}_populateEntityCheckboxes(e){const t=this._config.visible_sub_entities??{};for(const[,n]of Object.entries(e)){const e=n.type?this._entityContainers[n.type]:void 0;if(e&&(e.innerHTML="",n.entities))for(const[i,s]of Object.entries(n.entities)){if("sensor"===s.domain&&this._isChartEntity(i,s,n.type??""))continue;const o=document.createElement("div");o.style.cssText="display: flex; align-items: center; gap: 8px; margin-bottom: 5px; cursor: pointer;";const a=document.createElement("input");a.type="checkbox",a.checked=!0===t[i],a.style.cssText="width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary-color);";const r=document.createElement("span");let c=s.original_name??i;const l=n.name??"";c.startsWith(l+" ")&&(c=c.slice(l.length+1)),r.textContent=c,r.style.cssText="font-size: 0.85em; color: var(--primary-text-color); cursor: pointer;",o.appendChild(a),o.appendChild(r),e.appendChild(o),a.addEventListener("change",()=>{const e={...this._config.visible_sub_entities??{}};a.checked?e[i]=!0:delete e[i],this._config={...this._config,visible_sub_entities:e},this._fireConfigChanged()})}}}async _discoverAvailableRoles(e){if(this._hass&&e)try{const t=await this._hass.callWS({type:`${r}/panel_topology`,device_id:e}),n=new Set;for(const e of Object.values(t.circuits??{}))for(const t of Object.keys(e.entities??{}))n.add(t);this._availableRoles=n,this._populateMetricSelect(),t.sub_devices&&this._populateEntityCheckboxes(t.sub_devices)}catch{this._availableRoles=null,this._populateMetricSelect()}}_populateMetricSelect(){const e=this._metricSelect;if(!e)return;const t=this._config.chart_metric??s;e.innerHTML="";for(const[n,i]of Object.entries(g)){if(this._availableRoles&&!this._availableRoles.has(i.entityRole))continue;const s=document.createElement("option");s.value=n,s.textContent=i.label(),n===t&&(s.selected=!0),e.appendChild(s)}}_updateControls(){if(this._panelSelect&&(this._panelSelect.value=this._config.device_id??""),this._daysInput&&(this._daysInput.value=String(parseInt(String(this._config.history_days))||0)),this._hoursInput&&(this._hoursInput.value=String(parseInt(String(this._config.history_hours))||0)),this._minsInput&&(this._minsInput.value=String(parseInt(String(this._config.history_minutes))||0)),this._metricSelect&&(this._metricSelect.value=this._config.chart_metric??s),this._checkboxes)for(const[e,t]of Object.entries(this._checkboxes))t.checked=!1!==this._config[e];this._tabStyleSelect&&(this._tabStyleSelect.value=this._config.tab_style??"text")}_fireConfigChanged(){this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}}try{customElements.get("span-panel-card-editor")||customElements.define("span-panel-card-editor",Ot)}catch{}window.customCards=window.customCards??[],window.customCards.push({type:"span-panel-card",name:"SPAN Panel",description:"Physical panel layout with live power charts matching the SPAN frontend",preview:!0}),console.warn("%c SPAN-PANEL-CARD %c v0.9.2 ","background: var(--primary-color, #4dd9af); color: var(--text-primary-color, #000); font-weight: 700; padding: 2px 6px; border-radius: 4px 0 0 4px;","background: var(--secondary-background-color, #333); color: var(--primary-text-color, #fff); padding: 2px 6px; border-radius: 0 4px 4px 0;");
+ `}};Xt.styles=$('\n :host {\n --span-accent: var(--primary-color, #4dd9af);\n }\n\n ha-card {\n padding: 24px;\n background: var(--card-background-color, #1c1c1c);\n color: var(--primary-text-color, #e0e0e0);\n border-radius: var(--ha-card-border-radius, 12px);\n border: var(--ha-card-border-width, 1px) solid var(--ha-card-border-color, var(--divider-color, #333));\n box-shadow: var(--ha-card-box-shadow, none);\n }\n\n .panel-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px 16px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .header-left { flex: 1 1 300px; min-width: 0; }\n .header-center { flex: 0 0 auto; }\n .header-right { flex: 0 1 auto; min-width: 0; }\n\n .panel-identity {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px 12px;\n margin-bottom: 12px;\n }\n\n .panel-title {\n font-size: 1.8em;\n font-weight: 700;\n margin: 0;\n color: var(--primary-text-color, #fff);\n }\n\n .panel-serial {\n font-size: 0.85em;\n color: var(--secondary-text-color, #999);\n font-family: monospace;\n }\n\n .panel-stats {\n display: flex;\n flex-wrap: wrap;\n gap: 16px 32px;\n }\n\n /* Favorites view header: gear + slide-to-arm + right-anchored legend/W-A cluster. */\n .favorites-summary {\n padding: 8px 24px;\n border-bottom: 1px solid var(--divider-color, #e0e0e0);\n display: flex;\n align-items: center;\n gap: 12px;\n }\n /* Override the generic .gear-icon { margin-left: auto } rule so the\n favorites gear stays flush-left instead of floating to the right of\n the flex row (same idea as .panel-identity .panel-gear does for\n real-panel headers). */\n .favorites-summary .favorites-gear {\n margin-left: 0;\n }\n /* Right-anchored cluster wrapping the shedding legend + W/A unit toggle.\n margin-left:auto moved here from .favorites-summary-unit-toggle so the\n legend and toggle cluster together, matching the real-panel header\n layout. */\n .favorites-summary-right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 16px;\n }\n .favorites-subdevices-section {\n padding: 8px 16px 0;\n }\n\n /* Favorites view: responsive grid of per-contributing-panel status cards. */\n .favorites-panel-stats-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n padding: 12px 24px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .favorites-panel-card {\n background: var(--secondary-background-color, rgba(255, 255, 255, 0.04));\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n padding: 10px 14px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .favorites-panel-card-title {\n font-size: 0.85em;\n font-weight: 600;\n color: var(--primary-text-color);\n opacity: 0.85;\n }\n .favorites-panel-card .panel-stats {\n gap: 10px 20px;\n }\n .favorites-panel-card .stat-value {\n font-size: 1.15em;\n }\n\n .stat { display: flex; flex-direction: column; }\n .stat-label { font-size: 0.8em; color: var(--secondary-text-color, #999); margin-bottom: 2px; }\n .stat-row { display: flex; align-items: baseline; gap: 2px; }\n .stat-value { font-size: 1.5em; font-weight: 700; color: var(--primary-text-color, #fff); }\n .stat-unit { font-size: 0.7em; font-weight: 400; color: var(--secondary-text-color, #999); }\n\n .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; padding-top: 8px; }\n .header-right-top { display: flex; gap: 20px; align-items: center; }\n .meta-item { font-size: 0.8em; color: var(--secondary-text-color, #999); }\n\n .shedding-legend { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }\n .shedding-legend-item { display: inline-flex; align-items: center; gap: 3px; }\n .shedding-legend-item ha-icon { --mdc-icon-size: 16px; }\n .shedding-legend-secondary { --mdc-icon-size: 12px; opacity: 0.8; }\n .shedding-legend-text { font-size: 9px; font-weight: 600; }\n .shedding-legend-label { font-size: 0.7em; color: var(--secondary-text-color, #999); }\n\n .panel-gear {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color);\n opacity: 0.6;\n padding: 4px;\n margin-left: 8px;\n vertical-align: middle;\n }\n .panel-gear:hover { opacity: 1; }\n .header-center {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding-top: 8px;\n }\n .panel-identity .panel-gear {\n margin-left: 0;\n }\n .slide-confirm {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 160px;\n height: 28px;\n border-radius: 14px;\n background: color-mix(in srgb, var(--primary-color, #4dd9af) 20%, var(--secondary-background-color, #333));\n vertical-align: middle;\n overflow: hidden;\n user-select: none;\n touch-action: none;\n }\n .slide-confirm-text {\n position: absolute;\n width: 100%;\n text-align: center;\n font-size: 0.65em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n pointer-events: none;\n z-index: 0;\n }\n .slide-confirm-knob {\n position: absolute;\n left: 2px;\n top: 2px;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--secondary-text-color, #666);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: grab;\n z-index: 1;\n transition: none;\n }\n .slide-confirm-knob ha-icon {\n --mdc-icon-size: 14px;\n color: var(--card-background-color, #1c1c1c);\n }\n .slide-confirm-knob.snapping {\n transition: left 0.25s ease;\n }\n .slide-confirm.confirmed {\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n }\n .slide-confirm.confirmed .slide-confirm-text {\n color: var(--state-active-color, var(--span-accent));\n }\n .slide-confirm.confirmed .slide-confirm-knob {\n background: var(--state-active-color, var(--span-accent));\n }\n .switches-disabled .toggle-pill {\n opacity: 0.3;\n pointer-events: none;\n }\n .unit-toggle {\n display: inline-flex;\n background: var(--secondary-background-color, #333);\n border-radius: 6px;\n overflow: hidden;\n margin-left: 8px;\n }\n .unit-btn {\n padding: 4px 10px;\n border: none;\n background: none;\n color: var(--secondary-text-color);\n font-size: 0.75em;\n font-weight: 600;\n cursor: pointer;\n }\n .unit-btn.unit-active {\n background: var(--primary-color, #4dd9af);\n color: var(--text-primary-color, #000);\n }\n\n .monitoring-summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 16px;\n font-size: 0.8em;\n background: rgba(76, 175, 80, 0.1);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n }\n .monitoring-active { color: #4caf50; }\n .monitoring-counts { display: flex; gap: 12px; }\n .count-warning { color: #ff9800; }\n .count-alert { color: #f44336; }\n .count-overrides { color: var(--secondary-text-color); }\n\n .panel-grid {\n display: grid;\n grid-template-columns: 28px 1fr 1fr 28px;\n gap: 8px;\n align-items: stretch;\n }\n\n .tab-label {\n display: flex;\n align-items: center;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n user-select: none;\n }\n .tab-left { justify-content: flex-start; }\n .tab-right { justify-content: flex-end; }\n\n .circuit-slot {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px 20px;\n min-height: 140px;\n transition: opacity 0.3s;\n position: relative;\n overflow: hidden;\n }\n\n .circuit-col-span { min-height: 280px; }\n .circuit-row-span { border-left: 3px solid var(--span-accent); }\n .circuit-off .circuit-name,\n .circuit-off .breaker-badge,\n .circuit-off .power-value,\n .circuit-off .chart-container { opacity: 0.35; }\n .circuit-off .toggle-pill,\n .circuit-off .gear-icon { opacity: 1; }\n\n .circuit-empty {\n opacity: 0.2;\n min-height: 60px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-style: dashed;\n }\n .empty-label { color: var(--secondary-text-color, #999); font-size: 0.85em; }\n\n .circuit-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 6px;\n gap: 8px;\n }\n\n .circuit-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }\n\n .breaker-badge {\n background: color-mix(in srgb, var(--span-accent) 15%, transparent);\n color: var(--span-accent);\n font-size: 0.7em;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 4px;\n white-space: nowrap;\n border: 1px solid color-mix(in srgb, var(--span-accent) 25%, transparent);\n flex-shrink: 0;\n }\n\n .circuit-name {\n font-size: 0.9em;\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--primary-text-color, #e0e0e0);\n }\n\n .circuit-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n\n .power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .power-value strong { font-weight: 700; font-size: 1.1em; }\n .power-unit { font-size: 0.8em; font-weight: 400; color: var(--secondary-text-color, #999); margin-left: 1px; }\n .circuit-producer .power-value strong { color: var(--info-color, #4fc3f7); }\n\n .toggle-pill {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 4px;\n border-radius: 10px;\n cursor: pointer;\n font-size: 0.65em;\n font-weight: 600;\n transition: background 0.2s;\n user-select: none;\n min-width: 40px;\n }\n .toggle-on {\n padding-left: 6px;\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n color: var(--state-active-color, var(--span-accent));\n }\n .toggle-off {\n padding-right: 6px;\n background: color-mix(in srgb, var(--secondary-text-color) 15%, transparent);\n color: var(--secondary-text-color, #999);\n }\n .toggle-knob {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n transition: background 0.2s, margin 0.2s;\n }\n .toggle-on .toggle-knob {\n background: var(--state-active-color, var(--span-accent));\n margin-left: auto;\n }\n .toggle-off .toggle-knob {\n background: var(--secondary-text-color, #999);\n margin-right: auto;\n order: -1;\n }\n\n .circuit-status {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-top: 4px;\n padding: 0 4px;\n }\n .shedding-icon { opacity: 0.8; cursor: default; }\n .shedding-composite {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n }\n .shedding-icon-secondary { opacity: 0.8; }\n .shedding-label {\n font-size: 10px;\n font-weight: 600;\n opacity: 0.8;\n }\n .gear-icon {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n opacity: 0.6;\n transition: opacity 0.2s;\n margin-left: auto;\n }\n .gear-icon:hover { opacity: 1; }\n .utilization {\n font-size: 0.75em;\n font-weight: 600;\n }\n .utilization-normal { color: #4caf50; }\n .utilization-warning { color: #ff9800; }\n .utilization-alert { color: #f44336; }\n .circuit-alert {\n border-color: #f44336 !important;\n box-shadow: 0 0 8px rgba(244, 67, 54, 0.3);\n }\n .circuit-custom-monitoring {\n border-left: 3px solid #ff9800;\n }\n\n .chart-container {\n width: 100%;\n aspect-ratio: 4 / 1;\n margin-top: 4px;\n overflow: hidden;\n min-width: 0;\n }\n\n .sub-devices {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 12px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .sub-device {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px;\n }\n .sub-device-bess,\n .sub-device-full {\n grid-column: 1 / -1;\n }\n\n .sub-device-header { display: flex; gap: 10px; align-items: baseline; margin-bottom: 8px; }\n .sub-device-type { font-size: 0.7em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--span-accent); }\n .sub-device-name { font-size: 0.85em; color: var(--secondary-text-color, #999); flex: 1; }\n .sub-power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .sub-power-value strong { font-weight: 700; font-size: 1.1em; }\n .sub-device .chart-container { margin-bottom: 8px; aspect-ratio: auto; }\n\n .bess-charts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(0, 1fr));\n gap: 12px;\n margin-bottom: 10px;\n }\n .bess-chart-col { min-width: 0; }\n .bess-chart-title {\n font-size: 0.75em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--secondary-text-color, #999);\n margin-bottom: 4px;\n }\n .bess-chart-col .chart-container { aspect-ratio: auto; }\n .sub-entity { display: flex; gap: 6px; padding: 3px 0; font-size: 0.85em; }\n .sub-entity-name { color: var(--secondary-text-color, #999); }\n .sub-entity-value { font-weight: 500; color: var(--primary-text-color, #e0e0e0); }\n\n /* ── Shared tab bar ────────────────────────────────────── */\n\n .shared-tab-bar {\n display: flex;\n gap: 0;\n margin-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .shared-tab {\n padding: 8px 16px;\n cursor: pointer;\n font-size: 0.9em;\n font-weight: 500;\n color: var(--primary-text-color);\n opacity: 0.6;\n border: none;\n border-bottom: 2px solid transparent;\n background: none;\n transition: opacity 0.15s;\n }\n\n .shared-tab:hover {\n opacity: 0.85;\n }\n\n .shared-tab.active {\n opacity: 1;\n border-bottom-color: var(--span-accent);\n }\n\n /* ── List view search ──────────────────────────────────── */\n\n .list-search-container {\n margin-bottom: 12px;\n position: relative;\n }\n\n .list-search {\n width: 100%;\n padding: 8px 36px 8px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--secondary-background-color, #2a2a2a);\n color: var(--primary-text-color);\n font-size: 0.9em;\n box-sizing: border-box;\n outline: none;\n }\n\n .list-search:focus {\n border-color: var(--span-accent);\n }\n\n .list-search-clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 2px;\n display: flex;\n align-items: center;\n opacity: 0.7;\n }\n\n .list-search-clear:hover {\n opacity: 1;\n }\n\n .list-unit-toggle {\n display: inline-flex;\n margin-bottom: 12px;\n }\n\n /* ── List rows ─────────────────────────────────────────── */\n\n .list-view {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n /* Each circuit is wrapped in a .list-cell so the row + its optional\n expanded chart stay together. In single-column flex mode the cell\n just stacks naturally. In multi-column grid mode the cell becomes\n one grid item, so the chart is always in the same column as its\n row. Area headers (rendered as siblings, not inside a cell) span\n all columns via their inline "grid-column: 1 / -1". */\n .list-cell {\n display: flex;\n flex-direction: column;\n min-width: 0;\n }\n .list-view[data-columns="2"],\n .list-view[data-columns="3"] {\n display: grid;\n grid-template-columns: repeat(var(--list-cols), minmax(0, 1fr));\n gap: 6px 8px;\n flex-direction: initial;\n }\n /* On narrow viewports a 2/3-column list would squeeze rows into an\n unreadable shape, so force stacking regardless of user preference. */\n @media (max-width: 599px) {\n .list-view[data-columns="2"],\n .list-view[data-columns="3"] {\n display: flex;\n flex-direction: column;\n }\n }\n\n .list-row {\n display: flex;\n align-items: center;\n padding: 12px 16px;\n gap: 10px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n }\n\n .list-row:hover {\n background: var(--secondary-background-color, #2a2a2a);\n }\n\n .list-row.circuit-off {\n opacity: 0.5;\n }\n\n .list-row.list-row-expanded {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-bottom-color: transparent;\n }\n\n .list-circuit-name {\n flex: 1;\n color: var(--primary-text-color);\n font-size: 0.9em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .list-status-badge {\n font-size: 0.75em;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n flex-shrink: 0;\n }\n\n .list-status-on {\n color: #4dd9af;\n }\n\n .list-status-off {\n color: #f44336;\n }\n\n .list-power-value {\n font-size: 0.9em;\n font-weight: 600;\n min-width: 70px;\n text-align: right;\n flex-shrink: 0;\n }\n\n .list-expand-toggle {\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 4px;\n transition: transform 0.2s;\n display: flex;\n align-items: center;\n flex-shrink: 0;\n }\n\n .list-expand-toggle.expanded {\n transform: rotate(180deg);\n }\n\n .list-row .gear-icon {\n background: transparent;\n border: none;\n padding: 2px;\n cursor: pointer;\n color: #555;\n display: inline-flex;\n align-items: center;\n }\n .list-row .gear-icon:hover {\n color: var(--primary-text-color);\n }\n\n /* ── Expanded circuit content ──────────────────────────── */\n\n .list-expanded-content {\n padding: 0;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n border-radius: 0 0 8px 8px;\n margin-top: -6px;\n margin-bottom: 2px;\n }\n\n .circuit-slot.circuit-chart-only {\n border: none;\n margin: 0;\n background: none;\n padding: 8px 12px;\n min-height: 0;\n }\n\n /* ── Area headers ──────────────────────────────────────── */\n\n .area-header {\n padding: 16px 12px 6px;\n font-weight: 600;\n font-size: 0.85em;\n color: var(--secondary-text-color);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n /* ── No results ────────────────────────────────────────── */\n\n .list-no-results {\n padding: 24px;\n text-align: center;\n color: var(--secondary-text-color);\n }\n\n'),b([Ne({attribute:!1})],Xt.prototype,"hass",void 0),b([Me()],Xt.prototype,"_config",void 0),b([Me()],Xt.prototype,"_discovered",void 0),b([Me()],Xt.prototype,"_discovering",void 0),b([Me()],Xt.prototype,"_topology",void 0),b([Me()],Xt.prototype,"_activeTab",void 0),Xt=b([ze("span-panel-card")],Xt);class Zt extends HTMLElement{constructor(){super(...arguments),this._config={},this._hass=null,this._panels=null,this._availableRoles=null,this._built=!1,this._panelSelect=null,this._daysInput=null,this._hoursInput=null,this._minsInput=null,this._metricSelect=null,this._checkboxes={},this._entityContainers={},this._tabStyleSelect=null}setConfig(e){this._config={...e},this._updateControls()}set hass(e){this._hass=e,this._panels?this._built||this._buildEditor():this._discoverPanels()}async _discoverPanels(){if(!this._hass)return;const e=await this._hass.callWS({type:"config/device_registry/list"});this._panels=e.filter(e=>(e.identifiers??[]).some(e=>e[0]===l)&&!e.via_device_id).map(e=>{const t=(e.identifiers??[]).find(e=>e[0]===l)?.[1]??"",n=e.name_by_user??e.name??i("editor.panel_label");return{device_id:e.id,label:`${n} (${t})`}}),this._buildEditor()}_buildEditor(){this.innerHTML="",this._built=!0;const e=document.createElement("div");e.style.padding="16px";const t="\n width: 100%;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--card-background-color, var(--secondary-background-color, #1c1c1c));\n color: var(--primary-text-color, #e0e0e0);\n font-size: 1em;\n cursor: pointer;\n appearance: auto;\n box-sizing: border-box;\n ",n="display: block; font-weight: 500; margin-bottom: 8px; color: var(--primary-text-color);",i="margin-bottom: 16px;";this._buildPanelSelector(e,t,n,i),this._buildTimeWindow(e,t,n,i),this._buildMetricSelector(e,t,n,i),this._buildTabStyleSelector(e,t,n,i),this._buildSectionCheckboxes(e,n,i),this.appendChild(e),this._populateMetricSelect(),this._config.device_id&&this._discoverAvailableRoles(this._config.device_id)}_buildPanelSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const r=document.createElement("label");r.textContent=i("editor.panel_label"),r.style.cssText=n;const a=document.createElement("select");a.style.cssText=t;const l=document.createElement("option");if(l.value="",l.textContent=i("editor.select_panel"),a.appendChild(l),this._panels)for(const e of this._panels){const t=document.createElement("option");t.value=e.device_id,t.textContent=e.label,e.device_id===this._config.device_id&&(t.selected=!0),a.appendChild(t)}a.addEventListener("change",()=>{this._config={...this._config,device_id:a.value},this._fireConfigChanged(),this._discoverAvailableRoles(a.value)}),o.appendChild(r),o.appendChild(a),e.appendChild(o),this._panelSelect=a}_buildTimeWindow(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const r=document.createElement("label");r.textContent=i("editor.chart_window"),r.style.cssText=n;const a=document.createElement("div");a.style.cssText="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;";const l=t+"width: 70px; cursor: text;",c=(e,t,n,i)=>{const s=document.createElement("div");s.style.cssText="display: flex; align-items: center; gap: 6px;";const o=document.createElement("input");o.type="number",o.min=t,o.max=n,o.value=String(e),o.style.cssText=l;const r=document.createElement("span");return r.textContent=i,r.style.cssText="font-size: 0.9em; color: var(--secondary-text-color);",s.appendChild(o),s.appendChild(r),{wrap:s,input:o}},d=parseInt(String(this._config.history_days))||0,h=parseInt(String(this._config.history_hours))||0,p=parseInt(String(this._config.history_minutes))||0,u=c(d,"0","30",i("editor.days")),g=c(h,"0","23",i("editor.hours")),_=c(p,"0","59",i("editor.minutes")),f=()=>{this._config={...this._config,history_days:parseInt(u.input.value)||0,history_hours:parseInt(g.input.value)||0,history_minutes:parseInt(_.input.value)||0},this._fireConfigChanged()};u.input.addEventListener("change",f),g.input.addEventListener("change",f),_.input.addEventListener("change",f),a.appendChild(u.wrap),a.appendChild(g.wrap),a.appendChild(_.wrap),o.appendChild(r),o.appendChild(a),e.appendChild(o),this._daysInput=u.input,this._hoursInput=g.input,this._minsInput=_.input}_buildMetricSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const r=document.createElement("label");r.textContent=i("editor.chart_metric"),r.style.cssText=n;const a=document.createElement("select");a.style.cssText=t,a.addEventListener("change",()=>{this._config={...this._config,chart_metric:a.value},this._fireConfigChanged()}),o.appendChild(r),o.appendChild(a),e.appendChild(o),this._metricSelect=a}_buildTabStyleSelector(e,t,n,s){const o=document.createElement("div");o.style.cssText=s;const r=document.createElement("label");r.textContent=i("editor.tab_style"),r.style.cssText=n;const a=document.createElement("select");a.style.cssText=t;const l=[{value:"text",text:i("editor.tab_style_text")},{value:"icon",text:i("editor.tab_style_icon")}];for(const e of l){const t=document.createElement("option");t.value=e.value,t.textContent=e.text,e.value===(this._config.tab_style??"text")&&(t.selected=!0),a.appendChild(t)}a.addEventListener("change",()=>{this._config={...this._config,tab_style:a.value},this._fireConfigChanged()}),o.appendChild(r),o.appendChild(a),e.appendChild(o),this._tabStyleSelect=a}_buildSectionCheckboxes(e,t,n){const s=document.createElement("div");s.style.cssText=n;const o=document.createElement("label");o.textContent=i("editor.visible_sections"),o.style.cssText=t,s.appendChild(o);const r=[{key:"show_panel",label:i("editor.panel_circuits"),subDeviceType:null},{key:"show_battery",label:i("editor.battery_bess"),subDeviceType:"bess"},{key:"show_evse",label:i("editor.ev_charger_evse"),subDeviceType:"evse"}];this._checkboxes={},this._entityContainers={};for(const e of r){const t=document.createElement("div");t.style.cssText="display: flex; align-items: center; gap: 8px; margin-bottom: 6px; cursor: pointer;";const n=document.createElement("input");n.type="checkbox",n.checked=!1!==this._config[e.key],n.style.cssText="width: 18px; height: 18px; cursor: pointer; accent-color: var(--primary-color);";const i=document.createElement("span");i.textContent=e.label,i.style.cssText="font-size: 0.9em; color: var(--primary-text-color); cursor: pointer;",t.appendChild(n),t.appendChild(i),s.appendChild(t),this._checkboxes[e.key]=n;let o=null;e.subDeviceType&&(o=document.createElement("div"),o.style.cssText="padding-left: 26px;",o.style.display=n.checked?"block":"none",s.appendChild(o),this._entityContainers[e.subDeviceType]=o),n.addEventListener("change",()=>{this._config={...this._config,[e.key]:n.checked},o&&(o.style.display=n.checked?"block":"none"),this._fireConfigChanged()})}e.appendChild(s)}_isChartEntity(e,t,n){const i=(t.original_name??"").toLowerCase(),s=t.unique_id??"";if("power"===i||"battery power"===i||s.endsWith("_power"))return!0;if("bess"===n){if("battery level"===i||"battery percentage"===i||s.endsWith("_battery_level")||s.endsWith("_battery_percentage"))return!0;if("state of energy"===i||s.endsWith("_soe_kwh"))return!0;if("nameplate capacity"===i||s.endsWith("_nameplate_capacity"))return!0}return!1}_populateEntityCheckboxes(e){const t=this._config.visible_sub_entities??{};for(const[,n]of Object.entries(e)){const e=n.type?this._entityContainers[n.type]:void 0;if(e&&(e.innerHTML="",n.entities))for(const[i,s]of Object.entries(n.entities)){if("sensor"===s.domain&&this._isChartEntity(i,s,n.type??""))continue;const o=document.createElement("div");o.style.cssText="display: flex; align-items: center; gap: 8px; margin-bottom: 5px; cursor: pointer;";const r=document.createElement("input");r.type="checkbox",r.checked=!0===t[i],r.style.cssText="width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary-color);";const a=document.createElement("span");let l=s.original_name??i;const c=n.name??"";l.startsWith(c+" ")&&(l=l.slice(c.length+1)),a.textContent=l,a.style.cssText="font-size: 0.85em; color: var(--primary-text-color); cursor: pointer;",o.appendChild(r),o.appendChild(a),e.appendChild(o),r.addEventListener("change",()=>{const e={...this._config.visible_sub_entities??{}};r.checked?e[i]=!0:delete e[i],this._config={...this._config,visible_sub_entities:e},this._fireConfigChanged()})}}}async _discoverAvailableRoles(e){if(this._hass&&e)try{const t=await this._hass.callWS({type:`${l}/panel_topology`,device_id:e}),n=new Set;for(const e of Object.values(t.circuits??{}))for(const t of Object.keys(e.entities??{}))n.add(t);this._availableRoles=n,this._populateMetricSelect(),t.sub_devices&&this._populateEntityCheckboxes(t.sub_devices)}catch{this._availableRoles=null,this._populateMetricSelect()}}_populateMetricSelect(){const e=this._metricSelect;if(!e)return;const t=this._config.chart_metric??o;e.innerHTML="";for(const[n,i]of Object.entries(_)){if(this._availableRoles&&!this._availableRoles.has(i.entityRole))continue;const s=document.createElement("option");s.value=n,s.textContent=i.label(),n===t&&(s.selected=!0),e.appendChild(s)}}_updateControls(){if(this._panelSelect&&(this._panelSelect.value=this._config.device_id??""),this._daysInput&&(this._daysInput.value=String(parseInt(String(this._config.history_days))||0)),this._hoursInput&&(this._hoursInput.value=String(parseInt(String(this._config.history_hours))||0)),this._minsInput&&(this._minsInput.value=String(parseInt(String(this._config.history_minutes))||0)),this._metricSelect&&(this._metricSelect.value=this._config.chart_metric??o),this._checkboxes)for(const[e,t]of Object.entries(this._checkboxes))t.checked=!1!==this._config[e];this._tabStyleSelect&&(this._tabStyleSelect.value=this._config.tab_style??"text")}_fireConfigChanged(){this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}}try{customElements.get("span-panel-card-editor")||customElements.define("span-panel-card-editor",Zt)}catch{}window.customCards=window.customCards??[],window.customCards.push({type:"span-panel-card",name:"SPAN Panel",description:"Physical panel layout with live power charts matching the SPAN frontend",preview:!0}),console.warn("%c SPAN-PANEL-CARD %c v0.9.4 ","background: var(--primary-color, #4dd9af); color: var(--text-primary-color, #000); font-weight: 700; padding: 2px 6px; border-radius: 4px 0 0 4px;","background: var(--secondary-background-color, #333); color: var(--primary-text-color, #fff); padding: 2px 6px; border-radius: 0 4px 4px 0;");
diff --git a/custom_components/span_panel/frontend/dist/span-panel.js b/custom_components/span_panel/frontend/dist/span-panel.js
index 2717bb6c..c5e5b8bd 100644
--- a/custom_components/span_panel/frontend/dist/span-panel.js
+++ b/custom_components/span_panel/frontend/dist/span-panel.js
@@ -1,70 +1,144 @@
-let e="en";const t={en:{"tab.panel":"Panel","tab.by_panel":"By Panel","tab.by_activity":"By Activity","tab.by_area":"By Area","tab.monitoring":"Monitoring","tab.settings":"Settings","list.search_placeholder":"Search circuits...","list.unassigned_area":"Unassigned","list.no_results":"No circuits found","monitoring.heading":"Monitoring","monitoring.global_settings":"Global Settings","monitoring.enabled":"Enabled","monitoring.continuous":"Continuous (%)","monitoring.spike":"Spike (%)","monitoring.window":"Window (min)","monitoring.cooldown":"Cooldown (min)","monitoring.monitored_points":"Monitored Points","monitoring.col.name":"Name","monitoring.col.continuous":"Continuous","monitoring.col.spike":"Spike","monitoring.col.window":"Window","monitoring.col.cooldown":"Cooldown","monitoring.all_none":"All / None","monitoring.reset":"Reset","notification.heading":"Notification Settings","notification.targets":"Notify Targets","notification.none_selected":"None selected","notification.no_targets":"No notify targets found","notification.all_targets":"All","notification.event_bus_target":"Event Bus (HA event bus)","notification.priority":"Priority","notification.priority.default":"Default","notification.priority.passive":"Passive","notification.priority.active":"Active","notification.priority.time_sensitive":"Time-sensitive","notification.priority.critical":"Critical","notification.hint.critical":"Overrides silent/DND","notification.hint.time_sensitive":"Breaks through Focus","notification.hint.passive":"Delivers silently","notification.hint.active":"Standard delivery","notification.title_template":"Title Template","notification.message_template":"Message Template","notification.placeholders":"Placeholders:","notification.event_bus_help":"Event Bus fires event type","notification.event_bus_payload":"with payload:","notification.test_label":"Test Notification","notification.test_button":"Send Test","notification.test_sending":"Sending...","notification.test_sent":"Test notification sent","error.prefix":"Error:","error.failed_save":"Failed to save","error.failed":"Failed","settings.heading":"Settings","settings.description":"General integration settings (entity naming, device prefix, circuit numbers) are managed through the integration's options flow.","settings.open_link":"Open SPAN Panel Integration Settings","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","sidepanel.graph_horizon_failed":"Graph horizon update failed:","sidepanel.clear_graph_horizon_failed":"Clear graph horizon failed:","header.default_name":"SPAN Panel","header.monitoring_settings":"Panel monitoring settings","header.graph_settings":"Graph time horizon settings","header.site":"Site","header.grid":"Grid","header.upstream":"Upstream","header.downstream":"Downstream","header.solar":"Solar","header.battery":"Battery","header.toggle_units":"Toggle Watts / Amps","header.enable_switches":"Enable Switches","header.switches_enabled":"Switches Enabled","grid.unknown":"Unknown","grid.configure":"Configure circuit","grid.configure_subdevice":"Configure device","grid.on":"On","grid.off":"Off","subdevice.ev_charger":"EV Charger","subdevice.battery":"Battery","subdevice.fallback":"Sub-device","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Power","sidepanel.graph_settings":"Graph Settings","sidepanel.global_defaults":"Global defaults for all circuits","sidepanel.global_default":"Global Default","sidepanel.circuit_scales":"Circuit Graph Scales","sidepanel.subdevice_scales":"Sub-Device Graph Scales","sidepanel.reset_to_global":"Reset to global default","sidepanel.relay":"Relay","sidepanel.breaker":"Breaker","sidepanel.relay_failed":"Relay toggle failed:","sidepanel.shedding_priority":"Shedding Priority","sidepanel.priority_label":"Priority","sidepanel.shedding_failed":"Shedding update failed:","sidepanel.monitoring":"Monitoring","sidepanel.global":"Global","sidepanel.custom":"Custom","sidepanel.continuous_pct":"Continuous %","sidepanel.spike_pct":"Spike %","sidepanel.window_duration":"Window duration","sidepanel.cooldown":"Cooldown","sidepanel.monitoring_toggle_failed":"Monitoring toggle failed:","sidepanel.clear_monitoring_failed":"Clear monitoring failed:","sidepanel.save_threshold_failed":"Save threshold failed:","status.monitoring":"Monitoring","status.circuits":"circuits","status.mains":"mains","status.warning":"warning","status.warnings":"warnings","status.alert":"alert","status.alerts":"alerts","status.override":"override","status.overrides":"overrides","card.no_device":"Open the card editor and select your SPAN Panel device.","card.device_not_found":"Panel device not found. Check device_id in card config.","card.loading":"Loading...","card.topology_error":"Topology response missing panel_size and no circuits found. Update the SPAN Panel integration.","card.panel_size_error":"Could not determine panel_size. No circuits found and no panel_size attribute. Update the SPAN Panel integration.","editor.panel_label":"SPAN Panel","editor.select_panel":"Select a panel...","editor.chart_window":"Chart time window","editor.days":"days","editor.hours":"hours","editor.minutes":"minutes","editor.chart_metric":"Chart metric","editor.visible_sections":"Visible sections","editor.panel_circuits":"Panel circuits","editor.battery_bess":"Battery (BESS)","editor.ev_charger_evse":"EV Charger (EVSE)","editor.tab_style":"Tab Style","editor.tab_style_text":"Text","editor.tab_style_icon":"Icon","metric.power":"Power","metric.current":"Current","metric.soc":"State of Charge","metric.soe":"State of Energy","shedding.always_on":"Critical","shedding.never":"Non-sheddable","shedding.soc_threshold":"SoC Threshold","shedding.off_grid":"Sheddable","shedding.unknown":"Unknown","shedding.select.never":"Stays on in an outage","shedding.select.soc_threshold":"Stays on until battery threshold","shedding.select.off_grid":"Turns off in an outage"},es:{"tab.panel":"Panel","tab.by_panel":"Por Panel","tab.by_activity":"Por Actividad","tab.by_area":"Por Área","tab.monitoring":"Monitoreo","tab.settings":"Configuración","list.search_placeholder":"Buscar circuitos...","list.unassigned_area":"Sin asignar","list.no_results":"No se encontraron circuitos","monitoring.heading":"Monitoreo","monitoring.global_settings":"Configuración Global","monitoring.enabled":"Activado","monitoring.continuous":"Continuo (%)","monitoring.spike":"Pico (%)","monitoring.window":"Ventana (min)","monitoring.cooldown":"Enfriamiento (min)","monitoring.monitored_points":"Puntos Monitoreados","monitoring.col.name":"Nombre","monitoring.col.continuous":"Continuo","monitoring.col.spike":"Pico","monitoring.col.window":"Ventana","monitoring.col.cooldown":"Enfriamiento","monitoring.all_none":"Todos / Ninguno","monitoring.reset":"Restablecer","notification.heading":"Configuración de Notificaciones","notification.targets":"Destinos de Notificación","notification.none_selected":"Ninguno seleccionado","notification.no_targets":"No se encontraron destinos de notificación","notification.all_targets":"Todos","notification.event_bus_target":"Bus de Eventos (bus de eventos de HA)","notification.priority":"Prioridad","notification.priority.default":"Predeterminado","notification.priority.passive":"Pasivo","notification.priority.active":"Activo","notification.priority.time_sensitive":"Urgente","notification.priority.critical":"Crítico","notification.hint.critical":"Anula silencio/No molestar","notification.hint.time_sensitive":"Atraviesa el modo Concentración","notification.hint.passive":"Entrega silenciosa","notification.hint.active":"Entrega estándar","notification.title_template":"Plantilla de Título","notification.message_template":"Plantilla de Mensaje","notification.placeholders":"Variables:","notification.event_bus_help":"El Bus de Eventos dispara el tipo de evento","notification.event_bus_payload":"con datos:","notification.test_label":"Notificación de prueba","notification.test_button":"Enviar prueba","notification.test_sending":"Enviando...","notification.test_sent":"Notificación de prueba enviada","error.prefix":"Error:","error.failed_save":"Error al guardar","error.failed":"Falló","settings.heading":"Configuración","settings.description":"La configuración general de la integración (nombres de entidades, prefijo de dispositivo, números de circuito) se administra a través del flujo de opciones de la integración.","settings.open_link":"Abrir Configuración de Integración SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","sidepanel.graph_horizon_failed":"Graph horizon update failed:","sidepanel.clear_graph_horizon_failed":"Clear graph horizon failed:","header.default_name":"SPAN Panel","header.monitoring_settings":"Configuración de monitoreo del panel","header.graph_settings":"Configuración del horizonte temporal del gráfico","header.site":"Sitio","header.grid":"Red","header.upstream":"Aguas arriba","header.downstream":"Aguas abajo","header.solar":"Solar","header.battery":"Batería","header.toggle_units":"Alternar Watts / Amperios","header.enable_switches":"Habilitar Interruptores","header.switches_enabled":"Interruptores Habilitados","grid.unknown":"Desconocido","grid.configure":"Configurar circuito","grid.configure_subdevice":"Configurar dispositivo","grid.on":"Enc","grid.off":"Apag","subdevice.ev_charger":"Cargador EV","subdevice.battery":"Batería","subdevice.fallback":"Sub-dispositivo","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Potencia","sidepanel.graph_settings":"Configuración de Gráficos","sidepanel.global_defaults":"Valores predeterminados globales para todos los circuitos","sidepanel.global_default":"Predeterminado Global","sidepanel.circuit_scales":"Escalas de Gráficos de Circuitos","sidepanel.subdevice_scales":"Escalas de Gráficos de Sub-Dispositivos","sidepanel.reset_to_global":"Restablecer al valor global","sidepanel.relay":"Relé","sidepanel.breaker":"Interruptor","sidepanel.relay_failed":"Error al cambiar relé:","sidepanel.shedding_priority":"Prioridad de Desconexción","sidepanel.priority_label":"Prioridad","sidepanel.shedding_failed":"Error al actualizar desconexción:","sidepanel.monitoring":"Monitoreo","sidepanel.global":"Global","sidepanel.custom":"Personalizado","sidepanel.continuous_pct":"Continuo %","sidepanel.spike_pct":"Pico %","sidepanel.window_duration":"Duración de ventana","sidepanel.cooldown":"Enfriamiento","sidepanel.monitoring_toggle_failed":"Error al cambiar monitoreo:","sidepanel.clear_monitoring_failed":"Error al limpiar monitoreo:","sidepanel.save_threshold_failed":"Error al guardar umbral:","status.monitoring":"Monitoreo","status.circuits":"circuitos","status.mains":"alimentación","status.warning":"advertencia","status.warnings":"advertencias","status.alert":"alerta","status.alerts":"alertas","status.override":"anulación","status.overrides":"anulaciones","card.no_device":"Abra el editor de tarjeta y seleccione su dispositivo SPAN Panel.","card.device_not_found":"Dispositivo de panel no encontrado. Verifique device_id en la configuración de la tarjeta.","card.loading":"Cargando...","card.topology_error":"La respuesta de topología no contiene panel_size y no se encontraron circuitos. Actualice la integración SPAN Panel.","card.panel_size_error":"No se pudo determinar panel_size. No se encontraron circuitos ni atributo panel_size. Actualice la integración SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Seleccione un panel...","editor.chart_window":"Ventana de tiempo del gráfico","editor.days":"días","editor.hours":"horas","editor.minutes":"minutos","editor.chart_metric":"Métrica del gráfico","editor.visible_sections":"Secciones visibles","editor.panel_circuits":"Circuitos del panel","editor.battery_bess":"Batería (BESS)","editor.ev_charger_evse":"Cargador EV (EVSE)","editor.tab_style":"Estilo de pestañas","editor.tab_style_text":"Texto","editor.tab_style_icon":"Ícono","metric.power":"Potencia","metric.current":"Corriente","metric.soc":"Estado de Carga","metric.soe":"Estado de Energía","shedding.always_on":"Crítico","shedding.never":"No desconectable","shedding.soc_threshold":"Umbral SoC","shedding.off_grid":"Desconectable","shedding.unknown":"Desconocido","shedding.select.never":"Permanece encendido en un corte","shedding.select.soc_threshold":"Encendido hasta umbral de batería","shedding.select.off_grid":"Se apaga en un corte"},fr:{"tab.panel":"Panneau","tab.by_panel":"Par Panneau","tab.by_activity":"Par Activité","tab.by_area":"Par Zone","tab.monitoring":"Surveillance","tab.settings":"Paramètres","list.search_placeholder":"Rechercher des circuits...","list.unassigned_area":"Non attribué","list.no_results":"Aucun circuit trouvé","monitoring.heading":"Surveillance","monitoring.global_settings":"Paramètres Globaux","monitoring.enabled":"Activé","monitoring.continuous":"Continu (%)","monitoring.spike":"Pic (%)","monitoring.window":"Fenêtre (min)","monitoring.cooldown":"Refroidissement (min)","monitoring.monitored_points":"Points Surveillés","monitoring.col.name":"Nom","monitoring.col.continuous":"Continu","monitoring.col.spike":"Pic","monitoring.col.window":"Fenêtre","monitoring.col.cooldown":"Refroidissement","monitoring.all_none":"Tous / Aucun","monitoring.reset":"Réinitialiser","notification.heading":"Paramètres de Notification","notification.targets":"Cibles de Notification","notification.none_selected":"Aucune sélection","notification.no_targets":"Aucune cible de notification trouvée","notification.all_targets":"Tous","notification.event_bus_target":"Bus d'événements (bus d'événements HA)","notification.priority":"Priorité","notification.priority.default":"Par défaut","notification.priority.passive":"Passif","notification.priority.active":"Actif","notification.priority.time_sensitive":"Urgent","notification.priority.critical":"Critique","notification.hint.critical":"Outrepasse silencieux/NPD","notification.hint.time_sensitive":"Traverse le mode Concentration","notification.hint.passive":"Livraison silencieuse","notification.hint.active":"Livraison standard","notification.title_template":"Modèle de Titre","notification.message_template":"Modèle de Message","notification.placeholders":"Variables :","notification.event_bus_help":"Le Bus d'événements déclenche le type d'événement","notification.event_bus_payload":"avec les données :","notification.test_label":"Notification de test","notification.test_button":"Envoyer un test","notification.test_sending":"Envoi...","notification.test_sent":"Notification de test envoyée","error.prefix":"Erreur :","error.failed_save":"Échec de la sauvegarde","error.failed":"Échoué","settings.heading":"Paramètres","settings.description":"Les paramètres généraux de l'intégration (noms d'entités, préfixe de l'appareil, numéros de circuit) sont gérés via le flux d'options de l'intégration.","settings.open_link":"Ouvrir les Paramètres d'Intégration SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","sidepanel.graph_horizon_failed":"Graph horizon update failed:","sidepanel.clear_graph_horizon_failed":"Clear graph horizon failed:","header.default_name":"SPAN Panel","header.monitoring_settings":"Paramètres de surveillance du panneau","header.graph_settings":"Paramètres d'horizon temporel du graphique","header.site":"Site","header.grid":"Réseau","header.upstream":"Amont","header.downstream":"Aval","header.solar":"Solaire","header.battery":"Batterie","header.toggle_units":"Basculer Watts / Ampères","header.enable_switches":"Activer les interrupteurs","header.switches_enabled":"Interrupteurs activés","grid.unknown":"Inconnu","grid.configure":"Configurer le circuit","grid.configure_subdevice":"Configurer l'appareil","grid.on":"On","grid.off":"Off","subdevice.ev_charger":"Chargeur VE","subdevice.battery":"Batterie","subdevice.fallback":"Sous-appareil","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Puissance","sidepanel.graph_settings":"Paramètres des Graphiques","sidepanel.global_defaults":"Valeurs par défaut globales pour tous les circuits","sidepanel.global_default":"Défaut Global","sidepanel.circuit_scales":"Échelles des Graphiques de Circuits","sidepanel.subdevice_scales":"Échelles des Graphiques de Sous-Appareils","sidepanel.reset_to_global":"Réinitialiser à la valeur globale","sidepanel.relay":"Relais","sidepanel.breaker":"Disjoncteur","sidepanel.relay_failed":"Échec du basculement du relais :","sidepanel.shedding_priority":"Priorité de Délestage","sidepanel.priority_label":"Priorité","sidepanel.shedding_failed":"Échec de la mise à jour du délestage :","sidepanel.monitoring":"Surveillance","sidepanel.global":"Global","sidepanel.custom":"Personnalisé","sidepanel.continuous_pct":"Continu %","sidepanel.spike_pct":"Pic %","sidepanel.window_duration":"Durée de fenêtre","sidepanel.cooldown":"Refroidissement","sidepanel.monitoring_toggle_failed":"Échec du basculement de surveillance :","sidepanel.clear_monitoring_failed":"Échec de l'effacement de surveillance :","sidepanel.save_threshold_failed":"Échec de la sauvegarde du seuil :","status.monitoring":"Surveillance","status.circuits":"circuits","status.mains":"alimentation","status.warning":"avertissement","status.warnings":"avertissements","status.alert":"alerte","status.alerts":"alertes","status.override":"remplacement","status.overrides":"remplacements","card.no_device":"Ouvrez l'éditeur de carte et sélectionnez votre appareil SPAN Panel.","card.device_not_found":"Appareil de panneau introuvable. Vérifiez device_id dans la configuration de la carte.","card.loading":"Chargement...","card.topology_error":"La réponse de topologie ne contient pas panel_size et aucun circuit trouvé. Mettez à jour l'intégration SPAN Panel.","card.panel_size_error":"Impossible de déterminer panel_size. Aucun circuit trouvé et aucun attribut panel_size. Mettez à jour l'intégration SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Sélectionnez un panneau...","editor.chart_window":"Fenêtre de temps du graphique","editor.days":"jours","editor.hours":"heures","editor.minutes":"minutes","editor.chart_metric":"Métrique du graphique","editor.visible_sections":"Sections visibles","editor.panel_circuits":"Circuits du panneau","editor.battery_bess":"Batterie (BESS)","editor.ev_charger_evse":"Chargeur VE (EVSE)","editor.tab_style":"Style des onglets","editor.tab_style_text":"Texte","editor.tab_style_icon":"Icône","metric.power":"Puissance","metric.current":"Courant","metric.soc":"État de Charge","metric.soe":"État d'Énergie","shedding.always_on":"Critique","shedding.never":"Non délestable","shedding.soc_threshold":"Seuil SoC","shedding.off_grid":"Délestable","shedding.unknown":"Inconnu","shedding.select.never":"Reste allumé en cas de coupure","shedding.select.soc_threshold":"Allumé jusqu'au seuil batterie","shedding.select.off_grid":"S'éteint en cas de coupure"},ja:{"tab.panel":"パネル","tab.by_panel":"パネル別","tab.by_activity":"活動別","tab.by_area":"エリア別","tab.monitoring":"モニタリング","tab.settings":"設定","list.search_placeholder":"回路を検索...","list.unassigned_area":"未割り当て","list.no_results":"回路が見つかりません","monitoring.heading":"モニタリング","monitoring.global_settings":"グローバル設定","monitoring.enabled":"有効","monitoring.continuous":"継続 (%)","monitoring.spike":"スパイク (%)","monitoring.window":"ウィンドウ (分)","monitoring.cooldown":"クールダウン (分)","monitoring.monitored_points":"監視ポイント","monitoring.col.name":"名前","monitoring.col.continuous":"継続","monitoring.col.spike":"スパイク","monitoring.col.window":"ウィンドウ","monitoring.col.cooldown":"クールダウン","monitoring.all_none":"全選択 / 全解除","monitoring.reset":"リセット","notification.heading":"通知設定","notification.targets":"通知先","notification.none_selected":"未選択","notification.no_targets":"通知先が見つかりません","notification.all_targets":"すべて","notification.event_bus_target":"イベントバス (HAイベントバス)","notification.priority":"優先度","notification.priority.default":"デフォルト","notification.priority.passive":"パッシブ","notification.priority.active":"アクティブ","notification.priority.time_sensitive":"緊急","notification.priority.critical":"重大","notification.hint.critical":"サイレント/おやすみモードを無視","notification.hint.time_sensitive":"集中モードを突破","notification.hint.passive":"サイレント配信","notification.hint.active":"標準配信","notification.title_template":"タイトルテンプレート","notification.message_template":"メッセージテンプレート","notification.placeholders":"プレースホルダー:","notification.event_bus_help":"イベントバスが発行するイベントタイプ","notification.event_bus_payload":"ペイロード:","notification.test_label":"テスト通知","notification.test_button":"テスト送信","notification.test_sending":"送信中...","notification.test_sent":"テスト通知を送信しました","error.prefix":"エラー:","error.failed_save":"保存に失敗","error.failed":"失敗","settings.heading":"設定","settings.description":"統合の一般設定(エンティティ名、デバイスプレフィックス、回路番号)は統合のオプションフローで管理されます。","settings.open_link":"SPAN Panel統合設定を開く","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","sidepanel.graph_horizon_failed":"Graph horizon update failed:","sidepanel.clear_graph_horizon_failed":"Clear graph horizon failed:","header.default_name":"SPAN Panel","header.monitoring_settings":"パネルモニタリング設定","header.graph_settings":"グラフ時間範囲設定","header.site":"サイト","header.grid":"グリッド","header.upstream":"上流","header.downstream":"下流","header.solar":"ソーラー","header.battery":"バッテリー","header.toggle_units":"ワット/アンペア切り替え","header.enable_switches":"スイッチを有効化","header.switches_enabled":"スイッチ有効","grid.unknown":"不明","grid.configure":"回路を設定","grid.configure_subdevice":"デバイスを設定","grid.on":"オン","grid.off":"オフ","subdevice.ev_charger":"EV充電器","subdevice.battery":"バッテリー","subdevice.fallback":"サブデバイス","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"電力","sidepanel.graph_settings":"グラフ設定","sidepanel.global_defaults":"全回路のグローバルデフォルト","sidepanel.global_default":"グローバルデフォルト","sidepanel.circuit_scales":"回路グラフスケール","sidepanel.subdevice_scales":"サブデバイスグラフスケール","sidepanel.reset_to_global":"グローバルにリセット","sidepanel.relay":"リレー","sidepanel.breaker":"ブレーカー","sidepanel.relay_failed":"リレー切り替え失敗:","sidepanel.shedding_priority":"シェディング優先度","sidepanel.priority_label":"優先度","sidepanel.shedding_failed":"シェディング更新失敗:","sidepanel.monitoring":"モニタリング","sidepanel.global":"グローバル","sidepanel.custom":"カスタム","sidepanel.continuous_pct":"継続 %","sidepanel.spike_pct":"スパイク %","sidepanel.window_duration":"ウィンドウ時間","sidepanel.cooldown":"クールダウン","sidepanel.monitoring_toggle_failed":"モニタリング切り替え失敗:","sidepanel.clear_monitoring_failed":"モニタリングクリア失敗:","sidepanel.save_threshold_failed":"しきい値保存失敗:","status.monitoring":"モニタリング","status.circuits":"回路","status.mains":"主電源","status.warning":"警告","status.warnings":"警告","status.alert":"アラート","status.alerts":"アラート","status.override":"上書き","status.overrides":"上書き","card.no_device":"カードエディタを開いてSPAN Panelデバイスを選択してください。","card.device_not_found":"パネルデバイスが見つかりません。カード設定のdevice_idを確認してください。","card.loading":"読み込み中...","card.topology_error":"トポロジー応答にpanel_sizeがなく、回路が見つかりません。SPAN Panel統合を更新してください。","card.panel_size_error":"panel_sizeを判定できません。回路がpanel_size属性が見つかりません。SPAN Panel統合を更新してください。","editor.panel_label":"SPAN Panel","editor.select_panel":"パネルを選択...","editor.chart_window":"グラフ時間ウィンドウ","editor.days":"日","editor.hours":"時間","editor.minutes":"分","editor.chart_metric":"グラフ指標","editor.visible_sections":"表示セクション","editor.panel_circuits":"パネル回路","editor.battery_bess":"バッテリー (BESS)","editor.ev_charger_evse":"EV充電器 (EVSE)","editor.tab_style":"タブスタイル","editor.tab_style_text":"テキスト","editor.tab_style_icon":"アイコン","metric.power":"電力","metric.current":"電流","metric.soc":"充電状態","metric.soe":"エネルギー状態","shedding.always_on":"重要","shedding.never":"切断不可","shedding.soc_threshold":"SoCしきい値","shedding.off_grid":"切断可能","shedding.unknown":"不明","shedding.select.never":"停電時もオンを維持","shedding.select.soc_threshold":"バッテリーしきい値までオン","shedding.select.off_grid":"停電時にオフ"},pt:{"tab.panel":"Painel","tab.by_panel":"Por Painel","tab.by_activity":"Por Atividade","tab.by_area":"Por Área","tab.monitoring":"Monitoramento","tab.settings":"Configurações","list.search_placeholder":"Pesquisar circuitos...","list.unassigned_area":"Não atribuído","list.no_results":"Nenhum circuito encontrado","monitoring.heading":"Monitoramento","monitoring.global_settings":"Configurações Globais","monitoring.enabled":"Ativado","monitoring.continuous":"Contínuo (%)","monitoring.spike":"Pico (%)","monitoring.window":"Janela (min)","monitoring.cooldown":"Resfriamento (min)","monitoring.monitored_points":"Pontos Monitorados","monitoring.col.name":"Nome","monitoring.col.continuous":"Contínuo","monitoring.col.spike":"Pico","monitoring.col.window":"Janela","monitoring.col.cooldown":"Resfriamento","monitoring.all_none":"Todos / Nenhum","monitoring.reset":"Redefinir","notification.heading":"Configurações de Notificação","notification.targets":"Destinos de Notificação","notification.none_selected":"Nenhum selecionado","notification.no_targets":"Nenhum destino de notificação encontrado","notification.all_targets":"Todos","notification.event_bus_target":"Barramento de Eventos (barramento de eventos do HA)","notification.priority":"Prioridade","notification.priority.default":"Padrão","notification.priority.passive":"Passivo","notification.priority.active":"Ativo","notification.priority.time_sensitive":"Urgente","notification.priority.critical":"Crítico","notification.hint.critical":"Substitui silencioso/Não perturbar","notification.hint.time_sensitive":"Atravessa o modo Foco","notification.hint.passive":"Entrega silenciosa","notification.hint.active":"Entrega padrão","notification.title_template":"Modelo de Título","notification.message_template":"Modelo de Mensagem","notification.placeholders":"Variáveis:","notification.event_bus_help":"O Barramento de Eventos dispara o tipo de evento","notification.event_bus_payload":"com dados:","notification.test_label":"Notificação de teste","notification.test_button":"Enviar teste","notification.test_sending":"Enviando...","notification.test_sent":"Notificação de teste enviada","error.prefix":"Erro:","error.failed_save":"Falha ao salvar","error.failed":"Falhou","settings.heading":"Configurações","settings.description":"As configurações gerais da integração (nomes de entidades, prefixo do dispositivo, números de circuito) são gerenciadas através do fluxo de opções da integração.","settings.open_link":"Abrir Configurações de Integração SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","sidepanel.graph_horizon_failed":"Graph horizon update failed:","sidepanel.clear_graph_horizon_failed":"Clear graph horizon failed:","header.default_name":"SPAN Panel","header.monitoring_settings":"Configurações de monitoramento do painel","header.graph_settings":"Configurações do horizonte temporal do gráfico","header.site":"Local","header.grid":"Rede","header.upstream":"Montante","header.downstream":"Jusante","header.solar":"Solar","header.battery":"Bateria","header.toggle_units":"Alternar Watts / Amperes","header.enable_switches":"Ativar Interruptores","header.switches_enabled":"Interruptores Ativados","grid.unknown":"Desconhecido","grid.configure":"Configurar circuito","grid.configure_subdevice":"Configurar dispositivo","grid.on":"Lig","grid.off":"Des","subdevice.ev_charger":"Carregador VE","subdevice.battery":"Bateria","subdevice.fallback":"Sub-dispositivo","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Potência","sidepanel.graph_settings":"Configurações de Gráficos","sidepanel.global_defaults":"Padrões globais para todos os circuitos","sidepanel.global_default":"Padrão Global","sidepanel.circuit_scales":"Escalas de Gráficos de Circuitos","sidepanel.subdevice_scales":"Escalas de Gráficos de Sub-Dispositivos","sidepanel.reset_to_global":"Redefinir para o padrão global","sidepanel.relay":"Relé","sidepanel.breaker":"Disjuntor","sidepanel.relay_failed":"Falha ao alternar relé:","sidepanel.shedding_priority":"Prioridade de Desligamento","sidepanel.priority_label":"Prioridade","sidepanel.shedding_failed":"Falha ao atualizar desligamento:","sidepanel.monitoring":"Monitoramento","sidepanel.global":"Global","sidepanel.custom":"Personalizado","sidepanel.continuous_pct":"Contínuo %","sidepanel.spike_pct":"Pico %","sidepanel.window_duration":"Duração da janela","sidepanel.cooldown":"Resfriamento","sidepanel.monitoring_toggle_failed":"Falha ao alternar monitoramento:","sidepanel.clear_monitoring_failed":"Falha ao limpar monitoramento:","sidepanel.save_threshold_failed":"Falha ao salvar limite:","status.monitoring":"Monitoramento","status.circuits":"circuitos","status.mains":"alimentação","status.warning":"aviso","status.warnings":"avisos","status.alert":"alerta","status.alerts":"alertas","status.override":"substituição","status.overrides":"substituições","card.no_device":"Abra o editor do cartão e selecione seu dispositivo SPAN Panel.","card.device_not_found":"Dispositivo do painel não encontrado. Verifique device_id na configuração do cartão.","card.loading":"Carregando...","card.topology_error":"A resposta de topologia não contém panel_size e nenhum circuito encontrado. Atualize a integração SPAN Panel.","card.panel_size_error":"Não foi possível determinar panel_size. Nenhum circuito encontrado e nenhum atributo panel_size. Atualize a integração SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Selecione um painel...","editor.chart_window":"Janela de tempo do gráfico","editor.days":"dias","editor.hours":"horas","editor.minutes":"minutos","editor.chart_metric":"Métrica do gráfico","editor.visible_sections":"Seções visíveis","editor.panel_circuits":"Circuitos do painel","editor.battery_bess":"Bateria (BESS)","editor.ev_charger_evse":"Carregador VE (EVSE)","editor.tab_style":"Estilo das abas","editor.tab_style_text":"Texto","editor.tab_style_icon":"Ícone","metric.power":"Potência","metric.current":"Corrente","metric.soc":"Estado de Carga","metric.soe":"Estado de Energia","shedding.always_on":"Crítico","shedding.never":"Não desligável","shedding.soc_threshold":"Limite SoC","shedding.off_grid":"Desligável","shedding.unknown":"Desconhecido","shedding.select.never":"Permanece ligado em uma queda","shedding.select.soc_threshold":"Ligado até limite da bateria","shedding.select.off_grid":"Desliga em uma queda"}};function n(n){return t[e]?.[n]??t.en?.[n]??n}const i="power",o="5m",s={"5m":{ms:3e5,refreshMs:1e3,useRealtime:!0},"1h":{ms:36e5,refreshMs:3e4,useRealtime:!1},"1d":{ms:864e5,refreshMs:6e4,useRealtime:!1},"1w":{ms:6048e5,refreshMs:6e4,useRealtime:!1},"1M":{ms:2592e6,refreshMs:6e4,useRealtime:!1}},a="span_panel",r="CLOSED",l="pv",c="bess",d="evse",h="sub_",p=500,u={power:{entityRole:"power",label:()=>n("metric.power"),unit:e=>Math.abs(e)>=1e3?"kW":"W",format:e=>{const t=Math.abs(e);return t>=1e3?(t/1e3).toFixed(1):t<10&&t>0?t.toFixed(1):String(Math.round(t))}},current:{entityRole:"current",label:()=>n("metric.current"),unit:()=>"A",format:e=>Math.abs(e).toFixed(1)}},g={soc:{entityRole:"soc",label:()=>n("metric.soc"),unit:()=>"%",format:e=>String(Math.round(e)),fixedMin:0,fixedMax:100},soe:{entityRole:"soe",label:()=>n("metric.soe"),unit:()=>"kWh",format:e=>e.toFixed(1)},power:u.power},_={always_on:{icon:"mdi:battery",icon2:"mdi:router-wireless",color:"#4caf50",label:()=>n("shedding.always_on")},never:{icon:"mdi:battery",color:"#4caf50",label:()=>n("shedding.never")},soc_threshold:{icon:"mdi:battery-alert-variant-outline",color:"#9c27b0",label:()=>n("shedding.soc_threshold"),textLabel:"SoC"},off_grid:{icon:"mdi:transmission-tower",color:"#ff9800",label:()=>n("shedding.off_grid")},unknown:{icon:"mdi:help-circle-outline",color:"#888",label:()=>n("shedding.unknown")}},f="#ff9800";function m(e,t,n,i){var o,s=arguments.length,a=s<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,n):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)a=Reflect.decorate(e,t,n,i);else for(var r=e.length-1;r>=0;r--)(o=e[r])&&(a=(s<3?o(a):s>3?o(t,n,a):o(t,n))||a);return s>3&&a&&Object.defineProperty(t,n,a),a}"function"==typeof SuppressedError&&SuppressedError;
+let e="en";const t={en:{"tab.panel":"Panel","tab.by_panel":"By Panel","tab.by_activity":"By Activity","tab.by_area":"By Area","tab.monitoring":"Monitoring","tab.settings":"Settings","list.search_placeholder":"Search circuits...","list.unassigned_area":"Unassigned","list.no_results":"No circuits found","monitoring.heading":"Monitoring","monitoring.global_settings":"Global Settings","monitoring.enabled":"Enabled","monitoring.continuous":"Continuous (%)","monitoring.spike":"Spike (%)","monitoring.window":"Window (min)","monitoring.cooldown":"Cooldown (min)","monitoring.monitored_points":"Monitored Points","monitoring.col.name":"Name","monitoring.col.continuous":"Continuous","monitoring.col.spike":"Spike","monitoring.col.window":"Window","monitoring.col.cooldown":"Cooldown","monitoring.all_none":"All / None","monitoring.reset":"Reset","notification.heading":"Notification Settings","notification.targets":"Notify Targets","notification.none_selected":"None selected","notification.no_targets":"No notify targets found","notification.all_targets":"All","notification.event_bus_target":"Event Bus (HA event bus)","notification.priority":"Priority","notification.priority.default":"Default","notification.priority.passive":"Passive","notification.priority.active":"Active","notification.priority.time_sensitive":"Time-sensitive","notification.priority.critical":"Critical","notification.hint.critical":"Overrides silent/DND","notification.hint.time_sensitive":"Breaks through Focus","notification.hint.passive":"Delivers silently","notification.hint.active":"Standard delivery","notification.title_template":"Title Template","notification.message_template":"Message Template","notification.placeholders":"Placeholders:","notification.event_bus_help":"Event Bus fires event type","notification.event_bus_payload":"with payload:","notification.test_label":"Test Notification","notification.test_button":"Send Test","notification.test_sending":"Sending...","notification.test_sent":"Test notification sent","error.prefix":"Error:","error.failed_save":"Failed to save","error.failed":"Failed","error.panel_offline":"SPAN Panel unreachable","error.panel_reconnected":"SPAN Panel reconnected","error.panel_offline_named":"{name} unreachable","error.panel_reconnected_named":"{name} reconnected","error.discovery_failed":"Unable to connect to SPAN Panel","error.relay_failed":"Unable to toggle relay","error.shedding_failed":"Unable to update shedding priority","error.threshold_failed":"Unable to save threshold","error.graph_horizon_failed":"Unable to update graph time horizon","error.favorites_fetch_failed":"Unable to load favorites","error.favorites_toggle_failed":"Unable to update favorite","error.history_failed":"Unable to load historical data","error.monitoring_failed":"Unable to load monitoring status","error.graph_settings_failed":"Unable to load graph settings","error.areas_failed":"Area assignments may be out of sync","error.retry":"Retry","card.connecting":"Connecting to SPAN Panel...","settings.heading":"Settings","settings.description":"General integration settings (entity naming, device prefix, circuit numbers) are managed through the integration's options flow.","settings.open_link":"Open SPAN Panel Integration Settings","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","header.default_name":"SPAN Panel","header.monitoring_settings":"Panel monitoring settings","header.graph_settings":"Graph time horizon settings","header.site":"Site","header.grid":"Grid","header.upstream":"Upstream","header.downstream":"Downstream","header.solar":"Solar","header.battery":"Battery","header.toggle_units":"Toggle Watts / Amps","header.enable_switches":"Enable Switches","header.switches_enabled":"Switches Enabled","grid.unknown":"Unknown","grid.configure":"Configure circuit","grid.configure_subdevice":"Configure device","grid.on":"On","grid.off":"Off","subdevice.ev_charger":"EV Charger","subdevice.battery":"Battery","subdevice.fallback":"Sub-device","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Power","sidepanel.graph_settings":"Graph Settings","sidepanel.global_defaults":"Global defaults for all circuits","sidepanel.favorites_subtitle":"Favorites","sidepanel.global_default":"Global Default","sidepanel.list_view_columns":"List View Columns","sidepanel.columns":"Columns","sidepanel.circuit_scales":"Circuit Graph Scales","sidepanel.subdevice_scales":"Sub-Device Graph Scales","sidepanel.reset_to_global":"Reset to global default","sidepanel.relay":"Relay","sidepanel.breaker":"Breaker","sidepanel.shedding_priority":"Shedding Priority","sidepanel.priority_label":"Priority","sidepanel.monitoring":"Monitoring","sidepanel.global":"Global","sidepanel.custom":"Custom","sidepanel.continuous_pct":"Continuous %","sidepanel.spike_pct":"Spike %","sidepanel.window_duration":"Window duration","sidepanel.cooldown":"Cooldown","sidepanel.favorite":"Favorite","sidepanel.save_to_favorites":"Save to favorites","panel.favorites":"Favorites","status.monitoring":"Monitoring","status.circuits":"circuits","status.mains":"mains","status.warning":"warning","status.warnings":"warnings","status.alert":"alert","status.alerts":"alerts","status.override":"override","status.overrides":"overrides","card.no_device":"Open the card editor and select your SPAN Panel device.","card.device_not_found":"Panel device not found. Check device_id in card config.","card.topology_error":"Topology response missing panel_size and no circuits found. Update the SPAN Panel integration.","card.panel_size_error":"Could not determine panel_size. No circuits found and no panel_size attribute. Update the SPAN Panel integration.","editor.panel_label":"SPAN Panel","editor.select_panel":"Select a panel...","editor.chart_window":"Chart time window","editor.days":"days","editor.hours":"hours","editor.minutes":"minutes","editor.chart_metric":"Chart metric","editor.visible_sections":"Visible sections","editor.panel_circuits":"Panel circuits","editor.battery_bess":"Battery (BESS)","editor.ev_charger_evse":"EV Charger (EVSE)","editor.tab_style":"Tab Style","editor.tab_style_text":"Text","editor.tab_style_icon":"Icon","metric.power":"Power","metric.current":"Current","metric.soc":"State of Charge","metric.soe":"State of Energy","shedding.always_on":"Critical","shedding.never":"Non-sheddable","shedding.soc_threshold":"SoC Threshold","shedding.off_grid":"Sheddable","shedding.unknown":"Unknown","shedding.select.never":"Stays on in an outage","shedding.select.soc_threshold":"Stays on until battery threshold","shedding.select.off_grid":"Turns off in an outage"},es:{"tab.panel":"Panel","tab.by_panel":"Por Panel","tab.by_activity":"Por Actividad","tab.by_area":"Por Área","tab.monitoring":"Monitoreo","tab.settings":"Configuración","list.search_placeholder":"Buscar circuitos...","list.unassigned_area":"Sin asignar","list.no_results":"No se encontraron circuitos","monitoring.heading":"Monitoreo","monitoring.global_settings":"Configuración Global","monitoring.enabled":"Activado","monitoring.continuous":"Continuo (%)","monitoring.spike":"Pico (%)","monitoring.window":"Ventana (min)","monitoring.cooldown":"Enfriamiento (min)","monitoring.monitored_points":"Puntos Monitoreados","monitoring.col.name":"Nombre","monitoring.col.continuous":"Continuo","monitoring.col.spike":"Pico","monitoring.col.window":"Ventana","monitoring.col.cooldown":"Enfriamiento","monitoring.all_none":"Todos / Ninguno","monitoring.reset":"Restablecer","notification.heading":"Configuración de Notificaciones","notification.targets":"Destinos de Notificación","notification.none_selected":"Ninguno seleccionado","notification.no_targets":"No se encontraron destinos de notificación","notification.all_targets":"Todos","notification.event_bus_target":"Bus de Eventos (bus de eventos de HA)","notification.priority":"Prioridad","notification.priority.default":"Predeterminado","notification.priority.passive":"Pasivo","notification.priority.active":"Activo","notification.priority.time_sensitive":"Urgente","notification.priority.critical":"Crítico","notification.hint.critical":"Anula silencio/No molestar","notification.hint.time_sensitive":"Atraviesa el modo Concentración","notification.hint.passive":"Entrega silenciosa","notification.hint.active":"Entrega estándar","notification.title_template":"Plantilla de Título","notification.message_template":"Plantilla de Mensaje","notification.placeholders":"Variables:","notification.event_bus_help":"El Bus de Eventos dispara el tipo de evento","notification.event_bus_payload":"con datos:","notification.test_label":"Notificación de prueba","notification.test_button":"Enviar prueba","notification.test_sending":"Enviando...","notification.test_sent":"Notificación de prueba enviada","error.prefix":"Error:","error.failed_save":"Error al guardar","error.failed":"Falló","error.panel_offline":"SPAN Panel inaccesible","error.panel_reconnected":"SPAN Panel reconectado","error.panel_offline_named":"{name} inaccesible","error.panel_reconnected_named":"{name} reconectado","error.discovery_failed":"No se puede conectar al SPAN Panel","error.relay_failed":"No se pudo cambiar el relé","error.shedding_failed":"No se pudo actualizar la prioridad de desconexión","error.threshold_failed":"No se pudo guardar el umbral","error.graph_horizon_failed":"No se pudo actualizar el horizonte temporal del gráfico","error.favorites_fetch_failed":"No se pudieron cargar los favoritos","error.favorites_toggle_failed":"No se pudo actualizar el favorito","error.history_failed":"No se pudieron cargar los datos históricos","error.monitoring_failed":"No se pudo cargar el estado de monitoreo","error.graph_settings_failed":"No se pudo cargar la configuración del gráfico","error.areas_failed":"Las asignaciones de áreas pueden estar desincronizadas","error.retry":"Reintentar","card.connecting":"Conectando al SPAN Panel...","settings.heading":"Configuración","settings.description":"La configuración general de la integración (nombres de entidades, prefijo de dispositivo, números de circuito) se administra a través del flujo de opciones de la integración.","settings.open_link":"Abrir Configuración de Integración SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","header.default_name":"SPAN Panel","header.monitoring_settings":"Configuración de monitoreo del panel","header.graph_settings":"Configuración del horizonte temporal del gráfico","header.site":"Sitio","header.grid":"Red","header.upstream":"Aguas arriba","header.downstream":"Aguas abajo","header.solar":"Solar","header.battery":"Batería","header.toggle_units":"Alternar Watts / Amperios","header.enable_switches":"Habilitar Interruptores","header.switches_enabled":"Interruptores Habilitados","grid.unknown":"Desconocido","grid.configure":"Configurar circuito","grid.configure_subdevice":"Configurar dispositivo","grid.on":"Enc","grid.off":"Apag","subdevice.ev_charger":"Cargador EV","subdevice.battery":"Batería","subdevice.fallback":"Sub-dispositivo","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Potencia","sidepanel.graph_settings":"Configuración de Gráficos","sidepanel.global_defaults":"Valores predeterminados globales para todos los circuitos","sidepanel.favorites_subtitle":"Favoritos","sidepanel.global_default":"Predeterminado Global","sidepanel.list_view_columns":"Columnas de la lista","sidepanel.columns":"Columnas","sidepanel.circuit_scales":"Escalas de Gráficos de Circuitos","sidepanel.subdevice_scales":"Escalas de Gráficos de Sub-Dispositivos","sidepanel.reset_to_global":"Restablecer al valor global","sidepanel.relay":"Relé","sidepanel.breaker":"Interruptor","sidepanel.shedding_priority":"Prioridad de Desconexción","sidepanel.priority_label":"Prioridad","sidepanel.monitoring":"Monitoreo","sidepanel.global":"Global","sidepanel.custom":"Personalizado","sidepanel.continuous_pct":"Continuo %","sidepanel.spike_pct":"Pico %","sidepanel.window_duration":"Duración de ventana","sidepanel.cooldown":"Enfriamiento","sidepanel.favorite":"Favorito","sidepanel.save_to_favorites":"Guardar en favoritos","panel.favorites":"Favoritos","status.monitoring":"Monitoreo","status.circuits":"circuitos","status.mains":"alimentación","status.warning":"advertencia","status.warnings":"advertencias","status.alert":"alerta","status.alerts":"alertas","status.override":"anulación","status.overrides":"anulaciones","card.no_device":"Abra el editor de tarjeta y seleccione su dispositivo SPAN Panel.","card.device_not_found":"Dispositivo de panel no encontrado. Verifique device_id en la configuración de la tarjeta.","card.topology_error":"La respuesta de topología no contiene panel_size y no se encontraron circuitos. Actualice la integración SPAN Panel.","card.panel_size_error":"No se pudo determinar panel_size. No se encontraron circuitos ni atributo panel_size. Actualice la integración SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Seleccione un panel...","editor.chart_window":"Ventana de tiempo del gráfico","editor.days":"días","editor.hours":"horas","editor.minutes":"minutos","editor.chart_metric":"Métrica del gráfico","editor.visible_sections":"Secciones visibles","editor.panel_circuits":"Circuitos del panel","editor.battery_bess":"Batería (BESS)","editor.ev_charger_evse":"Cargador EV (EVSE)","editor.tab_style":"Estilo de pestañas","editor.tab_style_text":"Texto","editor.tab_style_icon":"Ícono","metric.power":"Potencia","metric.current":"Corriente","metric.soc":"Estado de Carga","metric.soe":"Estado de Energía","shedding.always_on":"Crítico","shedding.never":"No desconectable","shedding.soc_threshold":"Umbral SoC","shedding.off_grid":"Desconectable","shedding.unknown":"Desconocido","shedding.select.never":"Permanece encendido en un corte","shedding.select.soc_threshold":"Encendido hasta umbral de batería","shedding.select.off_grid":"Se apaga en un corte"},fr:{"tab.panel":"Panneau","tab.by_panel":"Par Panneau","tab.by_activity":"Par Activité","tab.by_area":"Par Zone","tab.monitoring":"Surveillance","tab.settings":"Paramètres","list.search_placeholder":"Rechercher des circuits...","list.unassigned_area":"Non attribué","list.no_results":"Aucun circuit trouvé","monitoring.heading":"Surveillance","monitoring.global_settings":"Paramètres Globaux","monitoring.enabled":"Activé","monitoring.continuous":"Continu (%)","monitoring.spike":"Pic (%)","monitoring.window":"Fenêtre (min)","monitoring.cooldown":"Refroidissement (min)","monitoring.monitored_points":"Points Surveillés","monitoring.col.name":"Nom","monitoring.col.continuous":"Continu","monitoring.col.spike":"Pic","monitoring.col.window":"Fenêtre","monitoring.col.cooldown":"Refroidissement","monitoring.all_none":"Tous / Aucun","monitoring.reset":"Réinitialiser","notification.heading":"Paramètres de Notification","notification.targets":"Cibles de Notification","notification.none_selected":"Aucune sélection","notification.no_targets":"Aucune cible de notification trouvée","notification.all_targets":"Tous","notification.event_bus_target":"Bus d'événements (bus d'événements HA)","notification.priority":"Priorité","notification.priority.default":"Par défaut","notification.priority.passive":"Passif","notification.priority.active":"Actif","notification.priority.time_sensitive":"Urgent","notification.priority.critical":"Critique","notification.hint.critical":"Outrepasse silencieux/NPD","notification.hint.time_sensitive":"Traverse le mode Concentration","notification.hint.passive":"Livraison silencieuse","notification.hint.active":"Livraison standard","notification.title_template":"Modèle de Titre","notification.message_template":"Modèle de Message","notification.placeholders":"Variables :","notification.event_bus_help":"Le Bus d'événements déclenche le type d'événement","notification.event_bus_payload":"avec les données :","notification.test_label":"Notification de test","notification.test_button":"Envoyer un test","notification.test_sending":"Envoi...","notification.test_sent":"Notification de test envoyée","error.prefix":"Erreur :","error.failed_save":"Échec de la sauvegarde","error.failed":"Échoué","error.panel_offline":"SPAN Panel inaccessible","error.panel_reconnected":"SPAN Panel reconnecté","error.panel_offline_named":"{name} inaccessible","error.panel_reconnected_named":"{name} reconnecté","error.discovery_failed":"Impossible de se connecter au SPAN Panel","error.relay_failed":"Impossible de basculer le relais","error.shedding_failed":"Impossible de mettre à jour la priorité de délestage","error.threshold_failed":"Impossible d'enregistrer le seuil","error.graph_horizon_failed":"Impossible de mettre à jour l'horizon temporel du graphique","error.favorites_fetch_failed":"Impossible de charger les favoris","error.favorites_toggle_failed":"Impossible de mettre à jour le favori","error.history_failed":"Impossible de charger les données historiques","error.monitoring_failed":"Impossible de charger l'état de surveillance","error.graph_settings_failed":"Impossible de charger les paramètres du graphique","error.areas_failed":"Les affectations de zones peuvent être désynchronisées","error.retry":"Réessayer","card.connecting":"Connexion au SPAN Panel...","settings.heading":"Paramètres","settings.description":"Les paramètres généraux de l'intégration (noms d'entités, préfixe de l'appareil, numéros de circuit) sont gérés via le flux d'options de l'intégration.","settings.open_link":"Ouvrir les Paramètres d'Intégration SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","header.default_name":"SPAN Panel","header.monitoring_settings":"Paramètres de surveillance du panneau","header.graph_settings":"Paramètres d'horizon temporel du graphique","header.site":"Site","header.grid":"Réseau","header.upstream":"Amont","header.downstream":"Aval","header.solar":"Solaire","header.battery":"Batterie","header.toggle_units":"Basculer Watts / Ampères","header.enable_switches":"Activer les interrupteurs","header.switches_enabled":"Interrupteurs activés","grid.unknown":"Inconnu","grid.configure":"Configurer le circuit","grid.configure_subdevice":"Configurer l'appareil","grid.on":"On","grid.off":"Off","subdevice.ev_charger":"Chargeur VE","subdevice.battery":"Batterie","subdevice.fallback":"Sous-appareil","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Puissance","sidepanel.graph_settings":"Paramètres des Graphiques","sidepanel.global_defaults":"Valeurs par défaut globales pour tous les circuits","sidepanel.favorites_subtitle":"Favoris","sidepanel.global_default":"Défaut Global","sidepanel.list_view_columns":"Colonnes de la liste","sidepanel.columns":"Colonnes","sidepanel.circuit_scales":"Échelles des Graphiques de Circuits","sidepanel.subdevice_scales":"Échelles des Graphiques de Sous-Appareils","sidepanel.reset_to_global":"Réinitialiser à la valeur globale","sidepanel.relay":"Relais","sidepanel.breaker":"Disjoncteur","sidepanel.shedding_priority":"Priorité de Délestage","sidepanel.priority_label":"Priorité","sidepanel.monitoring":"Surveillance","sidepanel.global":"Global","sidepanel.custom":"Personnalisé","sidepanel.continuous_pct":"Continu %","sidepanel.spike_pct":"Pic %","sidepanel.window_duration":"Durée de fenêtre","sidepanel.cooldown":"Refroidissement","sidepanel.favorite":"Favori","sidepanel.save_to_favorites":"Enregistrer dans les favoris","panel.favorites":"Favoris","status.monitoring":"Surveillance","status.circuits":"circuits","status.mains":"alimentation","status.warning":"avertissement","status.warnings":"avertissements","status.alert":"alerte","status.alerts":"alertes","status.override":"remplacement","status.overrides":"remplacements","card.no_device":"Ouvrez l'éditeur de carte et sélectionnez votre appareil SPAN Panel.","card.device_not_found":"Appareil de panneau introuvable. Vérifiez device_id dans la configuration de la carte.","card.topology_error":"La réponse de topologie ne contient pas panel_size et aucun circuit trouvé. Mettez à jour l'intégration SPAN Panel.","card.panel_size_error":"Impossible de déterminer panel_size. Aucun circuit trouvé et aucun attribut panel_size. Mettez à jour l'intégration SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Sélectionnez un panneau...","editor.chart_window":"Fenêtre de temps du graphique","editor.days":"jours","editor.hours":"heures","editor.minutes":"minutes","editor.chart_metric":"Métrique du graphique","editor.visible_sections":"Sections visibles","editor.panel_circuits":"Circuits du panneau","editor.battery_bess":"Batterie (BESS)","editor.ev_charger_evse":"Chargeur VE (EVSE)","editor.tab_style":"Style des onglets","editor.tab_style_text":"Texte","editor.tab_style_icon":"Icône","metric.power":"Puissance","metric.current":"Courant","metric.soc":"État de Charge","metric.soe":"État d'Énergie","shedding.always_on":"Critique","shedding.never":"Non délestable","shedding.soc_threshold":"Seuil SoC","shedding.off_grid":"Délestable","shedding.unknown":"Inconnu","shedding.select.never":"Reste allumé en cas de coupure","shedding.select.soc_threshold":"Allumé jusqu'au seuil batterie","shedding.select.off_grid":"S'éteint en cas de coupure"},ja:{"tab.panel":"パネル","tab.by_panel":"パネル別","tab.by_activity":"活動別","tab.by_area":"エリア別","tab.monitoring":"モニタリング","tab.settings":"設定","list.search_placeholder":"回路を検索...","list.unassigned_area":"未割り当て","list.no_results":"回路が見つかりません","monitoring.heading":"モニタリング","monitoring.global_settings":"グローバル設定","monitoring.enabled":"有効","monitoring.continuous":"継続 (%)","monitoring.spike":"スパイク (%)","monitoring.window":"ウィンドウ (分)","monitoring.cooldown":"クールダウン (分)","monitoring.monitored_points":"監視ポイント","monitoring.col.name":"名前","monitoring.col.continuous":"継続","monitoring.col.spike":"スパイク","monitoring.col.window":"ウィンドウ","monitoring.col.cooldown":"クールダウン","monitoring.all_none":"全選択 / 全解除","monitoring.reset":"リセット","notification.heading":"通知設定","notification.targets":"通知先","notification.none_selected":"未選択","notification.no_targets":"通知先が見つかりません","notification.all_targets":"すべて","notification.event_bus_target":"イベントバス (HAイベントバス)","notification.priority":"優先度","notification.priority.default":"デフォルト","notification.priority.passive":"パッシブ","notification.priority.active":"アクティブ","notification.priority.time_sensitive":"緊急","notification.priority.critical":"重大","notification.hint.critical":"サイレント/おやすみモードを無視","notification.hint.time_sensitive":"集中モードを突破","notification.hint.passive":"サイレント配信","notification.hint.active":"標準配信","notification.title_template":"タイトルテンプレート","notification.message_template":"メッセージテンプレート","notification.placeholders":"プレースホルダー:","notification.event_bus_help":"イベントバスが発行するイベントタイプ","notification.event_bus_payload":"ペイロード:","notification.test_label":"テスト通知","notification.test_button":"テスト送信","notification.test_sending":"送信中...","notification.test_sent":"テスト通知を送信しました","error.prefix":"エラー:","error.failed_save":"保存に失敗","error.failed":"失敗","error.panel_offline":"SPANパネルに接続できません","error.panel_reconnected":"SPANパネルが再接続されました","error.panel_offline_named":"{name}に接続できません","error.panel_reconnected_named":"{name}が再接続されました","error.discovery_failed":"SPANパネルへの接続に失敗しました","error.relay_failed":"リレーの切り替えに失敗しました","error.shedding_failed":"シェディング優先度の更新に失敗しました","error.threshold_failed":"しきい値の保存に失敗しました","error.graph_horizon_failed":"グラフの時間範囲の更新に失敗しました","error.favorites_fetch_failed":"お気に入りの読み込みに失敗しました","error.favorites_toggle_failed":"お気に入りの更新に失敗しました","error.history_failed":"履歴データの読み込みに失敗しました","error.monitoring_failed":"監視ステータスの読み込みに失敗しました","error.graph_settings_failed":"グラフ設定の読み込みに失敗しました","error.areas_failed":"エリア割り当てが同期されていない可能性があります","error.retry":"再試行","card.connecting":"SPANパネルに接続中...","settings.heading":"設定","settings.description":"統合の一般設定(エンティティ名、デバイスプレフィックス、回路番号)は統合のオプションフローで管理されます。","settings.open_link":"SPAN Panel統合設定を開く","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","header.default_name":"SPAN Panel","header.monitoring_settings":"パネルモニタリング設定","header.graph_settings":"グラフ時間範囲設定","header.site":"サイト","header.grid":"グリッド","header.upstream":"上流","header.downstream":"下流","header.solar":"ソーラー","header.battery":"バッテリー","header.toggle_units":"ワット/アンペア切り替え","header.enable_switches":"スイッチを有効化","header.switches_enabled":"スイッチ有効","grid.unknown":"不明","grid.configure":"回路を設定","grid.configure_subdevice":"デバイスを設定","grid.on":"オン","grid.off":"オフ","subdevice.ev_charger":"EV充電器","subdevice.battery":"バッテリー","subdevice.fallback":"サブデバイス","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"電力","sidepanel.graph_settings":"グラフ設定","sidepanel.global_defaults":"全回路のグローバルデフォルト","sidepanel.favorites_subtitle":"お気に入り","sidepanel.global_default":"グローバルデフォルト","sidepanel.list_view_columns":"リスト表示の列数","sidepanel.columns":"列","sidepanel.circuit_scales":"回路グラフスケール","sidepanel.subdevice_scales":"サブデバイスグラフスケール","sidepanel.reset_to_global":"グローバルにリセット","sidepanel.relay":"リレー","sidepanel.breaker":"ブレーカー","sidepanel.shedding_priority":"シェディング優先度","sidepanel.priority_label":"優先度","sidepanel.monitoring":"モニタリング","sidepanel.global":"グローバル","sidepanel.custom":"カスタム","sidepanel.continuous_pct":"継続 %","sidepanel.spike_pct":"スパイク %","sidepanel.window_duration":"ウィンドウ時間","sidepanel.cooldown":"クールダウン","sidepanel.favorite":"お気に入り","sidepanel.save_to_favorites":"お気に入りに保存","panel.favorites":"お気に入り","status.monitoring":"モニタリング","status.circuits":"回路","status.mains":"主電源","status.warning":"警告","status.warnings":"警告","status.alert":"アラート","status.alerts":"アラート","status.override":"上書き","status.overrides":"上書き","card.no_device":"カードエディタを開いてSPAN Panelデバイスを選択してください。","card.device_not_found":"パネルデバイスが見つかりません。カード設定のdevice_idを確認してください。","card.topology_error":"トポロジー応答にpanel_sizeがなく、回路が見つかりません。SPAN Panel統合を更新してください。","card.panel_size_error":"panel_sizeを判定できません。回路がpanel_size属性が見つかりません。SPAN Panel統合を更新してください。","editor.panel_label":"SPAN Panel","editor.select_panel":"パネルを選択...","editor.chart_window":"グラフ時間ウィンドウ","editor.days":"日","editor.hours":"時間","editor.minutes":"分","editor.chart_metric":"グラフ指標","editor.visible_sections":"表示セクション","editor.panel_circuits":"パネル回路","editor.battery_bess":"バッテリー (BESS)","editor.ev_charger_evse":"EV充電器 (EVSE)","editor.tab_style":"タブスタイル","editor.tab_style_text":"テキスト","editor.tab_style_icon":"アイコン","metric.power":"電力","metric.current":"電流","metric.soc":"充電状態","metric.soe":"エネルギー状態","shedding.always_on":"重要","shedding.never":"切断不可","shedding.soc_threshold":"SoCしきい値","shedding.off_grid":"切断可能","shedding.unknown":"不明","shedding.select.never":"停電時もオンを維持","shedding.select.soc_threshold":"バッテリーしきい値までオン","shedding.select.off_grid":"停電時にオフ"},pt:{"tab.panel":"Painel","tab.by_panel":"Por Painel","tab.by_activity":"Por Atividade","tab.by_area":"Por Área","tab.monitoring":"Monitoramento","tab.settings":"Configurações","list.search_placeholder":"Pesquisar circuitos...","list.unassigned_area":"Não atribuído","list.no_results":"Nenhum circuito encontrado","monitoring.heading":"Monitoramento","monitoring.global_settings":"Configurações Globais","monitoring.enabled":"Ativado","monitoring.continuous":"Contínuo (%)","monitoring.spike":"Pico (%)","monitoring.window":"Janela (min)","monitoring.cooldown":"Resfriamento (min)","monitoring.monitored_points":"Pontos Monitorados","monitoring.col.name":"Nome","monitoring.col.continuous":"Contínuo","monitoring.col.spike":"Pico","monitoring.col.window":"Janela","monitoring.col.cooldown":"Resfriamento","monitoring.all_none":"Todos / Nenhum","monitoring.reset":"Redefinir","notification.heading":"Configurações de Notificação","notification.targets":"Destinos de Notificação","notification.none_selected":"Nenhum selecionado","notification.no_targets":"Nenhum destino de notificação encontrado","notification.all_targets":"Todos","notification.event_bus_target":"Barramento de Eventos (barramento de eventos do HA)","notification.priority":"Prioridade","notification.priority.default":"Padrão","notification.priority.passive":"Passivo","notification.priority.active":"Ativo","notification.priority.time_sensitive":"Urgente","notification.priority.critical":"Crítico","notification.hint.critical":"Substitui silencioso/Não perturbar","notification.hint.time_sensitive":"Atravessa o modo Foco","notification.hint.passive":"Entrega silenciosa","notification.hint.active":"Entrega padrão","notification.title_template":"Modelo de Título","notification.message_template":"Modelo de Mensagem","notification.placeholders":"Variáveis:","notification.event_bus_help":"O Barramento de Eventos dispara o tipo de evento","notification.event_bus_payload":"com dados:","notification.test_label":"Notificação de teste","notification.test_button":"Enviar teste","notification.test_sending":"Enviando...","notification.test_sent":"Notificação de teste enviada","error.prefix":"Erro:","error.failed_save":"Falha ao salvar","error.failed":"Falhou","error.panel_offline":"SPAN Panel inacessível","error.panel_reconnected":"SPAN Panel reconectado","error.panel_offline_named":"{name} inacessível","error.panel_reconnected_named":"{name} reconectado","error.discovery_failed":"Não foi possível conectar ao SPAN Panel","error.relay_failed":"Não foi possível alternar o relé","error.shedding_failed":"Não foi possível atualizar a prioridade de desligamento","error.threshold_failed":"Não foi possível salvar o limite","error.graph_horizon_failed":"Não foi possível atualizar o horizonte temporal do gráfico","error.favorites_fetch_failed":"Não foi possível carregar os favoritos","error.favorites_toggle_failed":"Não foi possível atualizar o favorito","error.history_failed":"Não foi possível carregar os dados históricos","error.monitoring_failed":"Não foi possível carregar o status de monitoramento","error.graph_settings_failed":"Não foi possível carregar as configurações do gráfico","error.areas_failed":"As atribuições de áreas podem estar fora de sincronização","error.retry":"Tentar novamente","card.connecting":"Conectando ao SPAN Panel...","settings.heading":"Configurações","settings.description":"As configurações gerais da integração (nomes de entidades, prefixo do dispositivo, números de circuito) são gerenciadas através do fluxo de opções da integração.","settings.open_link":"Abrir Configurações de Integração SPAN Panel","horizon.5m":"5 Minutes","horizon.1h":"1 Hour","horizon.1d":"1 Day","horizon.1w":"1 Week","horizon.1M":"1 Month","settings.graph_horizon_heading":"Graph Time Horizon","settings.graph_horizon_description":"Default time window for all circuit graphs. Individual circuits can override this in their settings panel.","settings.global_default":"Global Default","settings.default_scale":"Default Scale","settings.circuit_graph_scales":"Circuit Graph Scales","settings.col.circuit":"Circuit","settings.col.scale":"Scale","sidepanel.graph_horizon":"Graph Time Horizon","header.default_name":"SPAN Panel","header.monitoring_settings":"Configurações de monitoramento do painel","header.graph_settings":"Configurações do horizonte temporal do gráfico","header.site":"Local","header.grid":"Rede","header.upstream":"Montante","header.downstream":"Jusante","header.solar":"Solar","header.battery":"Bateria","header.toggle_units":"Alternar Watts / Amperes","header.enable_switches":"Ativar Interruptores","header.switches_enabled":"Interruptores Ativados","grid.unknown":"Desconhecido","grid.configure":"Configurar circuito","grid.configure_subdevice":"Configurar dispositivo","grid.on":"Lig","grid.off":"Des","subdevice.ev_charger":"Carregador VE","subdevice.battery":"Bateria","subdevice.fallback":"Sub-dispositivo","subdevice.soc":"SoC","subdevice.soe":"SoE","subdevice.power":"Potência","sidepanel.graph_settings":"Configurações de Gráficos","sidepanel.global_defaults":"Padrões globais para todos os circuitos","sidepanel.favorites_subtitle":"Favoritos","sidepanel.global_default":"Padrão Global","sidepanel.list_view_columns":"Colunas da Lista","sidepanel.columns":"Colunas","sidepanel.circuit_scales":"Escalas de Gráficos de Circuitos","sidepanel.subdevice_scales":"Escalas de Gráficos de Sub-Dispositivos","sidepanel.reset_to_global":"Redefinir para o padrão global","sidepanel.relay":"Relé","sidepanel.breaker":"Disjuntor","sidepanel.shedding_priority":"Prioridade de Desligamento","sidepanel.priority_label":"Prioridade","sidepanel.monitoring":"Monitoramento","sidepanel.global":"Global","sidepanel.custom":"Personalizado","sidepanel.continuous_pct":"Contínuo %","sidepanel.spike_pct":"Pico %","sidepanel.window_duration":"Duração da janela","sidepanel.cooldown":"Resfriamento","sidepanel.favorite":"Favorito","sidepanel.save_to_favorites":"Salvar nos favoritos","panel.favorites":"Favoritos","status.monitoring":"Monitoramento","status.circuits":"circuitos","status.mains":"alimentação","status.warning":"aviso","status.warnings":"avisos","status.alert":"alerta","status.alerts":"alertas","status.override":"substituição","status.overrides":"substituições","card.no_device":"Abra o editor do cartão e selecione seu dispositivo SPAN Panel.","card.device_not_found":"Dispositivo do painel não encontrado. Verifique device_id na configuração do cartão.","card.topology_error":"A resposta de topologia não contém panel_size e nenhum circuito encontrado. Atualize a integração SPAN Panel.","card.panel_size_error":"Não foi possível determinar panel_size. Nenhum circuito encontrado e nenhum atributo panel_size. Atualize a integração SPAN Panel.","editor.panel_label":"SPAN Panel","editor.select_panel":"Selecione um painel...","editor.chart_window":"Janela de tempo do gráfico","editor.days":"dias","editor.hours":"horas","editor.minutes":"minutos","editor.chart_metric":"Métrica do gráfico","editor.visible_sections":"Seções visíveis","editor.panel_circuits":"Circuitos do painel","editor.battery_bess":"Bateria (BESS)","editor.ev_charger_evse":"Carregador VE (EVSE)","editor.tab_style":"Estilo das abas","editor.tab_style_text":"Texto","editor.tab_style_icon":"Ícone","metric.power":"Potência","metric.current":"Corrente","metric.soc":"Estado de Carga","metric.soe":"Estado de Energia","shedding.always_on":"Crítico","shedding.never":"Não desligável","shedding.soc_threshold":"Limite SoC","shedding.off_grid":"Desligável","shedding.unknown":"Desconhecido","shedding.select.never":"Permanece ligado em uma queda","shedding.select.soc_threshold":"Ligado até limite da bateria","shedding.select.off_grid":"Desliga em uma queda"}};function n(n){return t[e]?.[n]??t.en?.[n]??n}function i(n,i){return(t[e]?.[n]??t.en?.[n]??n).replace(/\{(\w+)\}/g,(e,t)=>Object.prototype.hasOwnProperty.call(i,t)?i[t]:`{${t}}`)}const s="power",o="5m",r={"5m":{ms:3e5,refreshMs:1e3,useRealtime:!0},"1h":{ms:36e5,refreshMs:3e4,useRealtime:!1},"1d":{ms:864e5,refreshMs:6e4,useRealtime:!1},"1w":{ms:6048e5,refreshMs:6e4,useRealtime:!1},"1M":{ms:2592e6,refreshMs:6e4,useRealtime:!1}},a="span_panel",l="CLOSED",c="pv",d="bess",h="evse",p="sub_",u=500,g={power:{entityRole:"power",label:()=>n("metric.power"),unit:e=>Math.abs(e)>=1e3?"kW":"W",format:e=>{const t=Math.abs(e);return t>=1e3?(t/1e3).toFixed(1):t<10&&t>0?t.toFixed(1):String(Math.round(t))}},current:{entityRole:"current",label:()=>n("metric.current"),unit:()=>"A",format:e=>Math.abs(e).toFixed(1)}},_={soc:{entityRole:"soc",label:()=>n("metric.soc"),unit:()=>"%",format:e=>String(Math.round(e)),fixedMin:0,fixedMax:100},soe:{entityRole:"soe",label:()=>n("metric.soe"),unit:()=>"kWh",format:e=>e.toFixed(1)},power:g.power},f={always_on:{icon:"mdi:battery",icon2:"mdi:router-wireless",color:"#4caf50",label:()=>n("shedding.always_on")},never:{icon:"mdi:battery",color:"#4caf50",label:()=>n("shedding.never")},soc_threshold:{icon:"mdi:battery-alert-variant-outline",color:"#9c27b0",label:()=>n("shedding.soc_threshold"),textLabel:"SoC"},off_grid:{icon:"mdi:transmission-tower",color:"#ff9800",label:()=>n("shedding.off_grid")},unknown:{icon:"mdi:help-circle-outline",color:"#888",label:()=>n("shedding.unknown")}},v="#ff9800";function m(e,t,n,i){var s,o=arguments.length,r=o<3?t:null===i?i=Object.getOwnPropertyDescriptor(t,n):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(e,t,n,i);else for(var a=e.length-1;a>=0;a--)(s=e[a])&&(r=(o<3?s(r):o>3?s(t,n,r):s(t,n))||r);return o>3&&r&&Object.defineProperty(t,n,r),r}"function"==typeof SuppressedError&&SuppressedError;
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
-const b=globalThis,v=b.ShadowRoot&&(void 0===b.ShadyCSS||b.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,y=Symbol(),w=new WeakMap;let x=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==y)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(v&&void 0===e){const n=void 0!==t&&1===t.length;n&&(e=w.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),n&&w.set(t,e))}return e}toString(){return this.cssText}};const $=v?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const n of e.cssRules)t+=n.cssText;return(e=>new x("string"==typeof e?e:e+"",void 0,y))(t)})(e):e,{is:S,defineProperty:C,getOwnPropertyDescriptor:E,getOwnPropertyNames:k,getOwnPropertySymbols:z,getPrototypeOf:A}=Object,P=globalThis,M=P.trustedTypes,T=M?M.emptyScript:"",N=P.reactiveElementPolyfillSupport,D=(e,t)=>e,L={toAttribute(e,t){switch(t){case Boolean:e=e?T:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=null!==e;break;case Number:n=null===e?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch(e){n=null}}return n}},H=(e,t)=>!S(e,t),I={attribute:!0,type:String,converter:L,reflect:!1,useDefault:!1,hasChanged:H};
+const b=globalThis,y=b.ShadowRoot&&(void 0===b.ShadyCSS||b.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,w=Symbol(),x=new WeakMap;let S=class{constructor(e,t,n){if(this._$cssResult$=!0,n!==w)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(y&&void 0===e){const n=void 0!==t&&1===t.length;n&&(e=x.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),n&&x.set(t,e))}return e}toString(){return this.cssText}};const $=e=>new S("string"==typeof e?e:e+"",void 0,w),C=(e,...t)=>{const n=1===e.length?e[0]:t.reduce((t,n,i)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+e[i+1],e[0]);return new S(n,e,w)},P=y?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const n of e.cssRules)t+=n.cssText;return $(t)})(e):e,{is:k,defineProperty:E,getOwnPropertyDescriptor:z,getOwnPropertyNames:A,getOwnPropertySymbols:N,getPrototypeOf:M}=Object,I=globalThis,T=I.trustedTypes,D=T?T.emptyScript:"",F=I.reactiveElementPolyfillSupport,L=(e,t)=>e,H={toAttribute(e,t){switch(t){case Boolean:e=e?D:null;break;case Object:case Array:e=null==e?e:JSON.stringify(e)}return e},fromAttribute(e,t){let n=e;switch(t){case Boolean:n=null!==e;break;case Number:n=null===e?null:Number(e);break;case Object:case Array:try{n=JSON.parse(e)}catch(e){n=null}}return n}},O=(e,t)=>!k(e,t),R={attribute:!0,type:String,converter:H,reflect:!1,useDefault:!1,hasChanged:O};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
- */Symbol.metadata??=Symbol("metadata"),P.litPropertyMetadata??=new WeakMap;let O=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=I){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const n=Symbol(),i=this.getPropertyDescriptor(e,n,t);void 0!==i&&C(this.prototype,e,i)}}static getPropertyDescriptor(e,t,n){const{get:i,set:o}=E(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:i,set(t){const s=i?.call(this);o?.call(this,t),this.requestUpdate(e,s,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??I}static _$Ei(){if(this.hasOwnProperty(D("elementProperties")))return;const e=A(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(D("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(D("properties"))){const e=this.properties,t=[...k(e),...z(e)];for(const n of t)this.createProperty(n,e[n])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,n]of t)this.elementProperties.set(e,n)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const n=this._$Eu(e,t);void 0!==n&&this._$Eh.set(n,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const n=new Set(e.flat(1/0).reverse());for(const e of n)t.unshift($(e))}else void 0!==e&&t.push($(e));return t}static _$Eu(e,t){const n=t.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const n of t.keys())this.hasOwnProperty(n)&&(e.set(n,this[n]),delete this[n]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{if(v)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const n of t){const t=document.createElement("style"),i=b.litNonce;void 0!==i&&t.setAttribute("nonce",i),t.textContent=n.cssText,e.appendChild(t)}})(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,n){this._$AK(e,n)}_$ET(e,t){const n=this.constructor.elementProperties.get(e),i=this.constructor._$Eu(e,n);if(void 0!==i&&!0===n.reflect){const o=(void 0!==n.converter?.toAttribute?n.converter:L).toAttribute(t,n.type);this._$Em=e,null==o?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(e,t){const n=this.constructor,i=n._$Eh.get(e);if(void 0!==i&&this._$Em!==i){const e=n.getPropertyOptions(i),o="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:L;this._$Em=i;const s=o.fromAttribute(t,e.type);this[i]=s??this._$Ej?.get(i)??s,this._$Em=null}}requestUpdate(e,t,n,i=!1,o){if(void 0!==e){const s=this.constructor;if(!1===i&&(o=this[e]),n??=s.getPropertyOptions(e),!((n.hasChanged??H)(o,t)||n.useDefault&&n.reflect&&o===this._$Ej?.get(e)&&!this.hasAttribute(s._$Eu(e,n))))return;this.C(e,t,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:n,reflect:i,wrapped:o},s){n&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,s??t??this[e]),!0!==o||void 0!==s)||(this._$AL.has(e)||(this.hasUpdated||n||(t=void 0),this._$AL.set(e,t)),!0===i&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,n]of e){const{wrapped:e}=n,i=this[t];!0!==e||this._$AL.has(t)||void 0===i||this.C(t,void 0,n,i)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};O.elementStyles=[],O.shadowRootOptions={mode:"open"},O[D("elementProperties")]=new Map,O[D("finalized")]=new Map,N?.({ReactiveElement:O}),(P.reactiveElementVersions??=[]).push("2.1.2");
+ */Symbol.metadata??=Symbol("metadata"),I.litPropertyMetadata??=new WeakMap;let q=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=R){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){const n=Symbol(),i=this.getPropertyDescriptor(e,n,t);void 0!==i&&E(this.prototype,e,i)}}static getPropertyDescriptor(e,t,n){const{get:i,set:s}=z(this.prototype,e)??{get(){return this[t]},set(e){this[t]=e}};return{get:i,set(t){const o=i?.call(this);s?.call(this,t),this.requestUpdate(e,o,n)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??R}static _$Ei(){if(this.hasOwnProperty(L("elementProperties")))return;const e=M(this);e.finalize(),void 0!==e.l&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(L("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(L("properties"))){const e=this.properties,t=[...A(e),...N(e)];for(const n of t)this.createProperty(n,e[n])}const e=this[Symbol.metadata];if(null!==e){const t=litPropertyMetadata.get(e);if(void 0!==t)for(const[e,n]of t)this.elementProperties.set(e,n)}this._$Eh=new Map;for(const[e,t]of this.elementProperties){const n=this._$Eu(e,t);void 0!==n&&this._$Eh.set(n,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const n=new Set(e.flat(1/0).reverse());for(const e of n)t.unshift(P(e))}else void 0!==e&&t.push(P(e));return t}static _$Eu(e,t){const n=t.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof e?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),void 0!==this.renderRoot&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){const e=new Map,t=this.constructor.elementProperties;for(const n of t.keys())this.hasOwnProperty(n)&&(e.set(n,this[n]),delete this[n]);e.size>0&&(this._$Ep=e)}createRenderRoot(){const e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return((e,t)=>{if(y)e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(const n of t){const t=document.createElement("style"),i=b.litNonce;void 0!==i&&t.setAttribute("nonce",i),t.textContent=n.cssText,e.appendChild(t)}})(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,n){this._$AK(e,n)}_$ET(e,t){const n=this.constructor.elementProperties.get(e),i=this.constructor._$Eu(e,n);if(void 0!==i&&!0===n.reflect){const s=(void 0!==n.converter?.toAttribute?n.converter:H).toAttribute(t,n.type);this._$Em=e,null==s?this.removeAttribute(i):this.setAttribute(i,s),this._$Em=null}}_$AK(e,t){const n=this.constructor,i=n._$Eh.get(e);if(void 0!==i&&this._$Em!==i){const e=n.getPropertyOptions(i),s="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==e.converter?.fromAttribute?e.converter:H;this._$Em=i;const o=s.fromAttribute(t,e.type);this[i]=o??this._$Ej?.get(i)??o,this._$Em=null}}requestUpdate(e,t,n,i=!1,s){if(void 0!==e){const o=this.constructor;if(!1===i&&(s=this[e]),n??=o.getPropertyOptions(e),!((n.hasChanged??O)(s,t)||n.useDefault&&n.reflect&&s===this._$Ej?.get(e)&&!this.hasAttribute(o._$Eu(e,n))))return;this.C(e,t,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(e,t,{useDefault:n,reflect:i,wrapped:s},o){n&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,o??t??this[e]),!0!==s||void 0!==o)||(this._$AL.has(e)||(this.hasUpdated||n||(t=void 0),this._$AL.set(e,t)),!0===i&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[e,t]of this._$Ep)this[e]=t;this._$Ep=void 0}const e=this.constructor.elementProperties;if(e.size>0)for(const[t,n]of e){const{wrapped:e}=n,i=this[t];!0!==e||this._$AL.has(t)||void 0===i||this.C(t,void 0,n,i)}}let e=!1;const t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(e=>e.hostUpdate?.()),this.update(t)):this._$EM()}catch(t){throw e=!1,this._$EM(),t}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(e){}firstUpdated(e){}};q.elementStyles=[],q.shadowRootOptions={mode:"open"},q[L("elementProperties")]=new Map,q[L("finalized")]=new Map,F?.({ReactiveElement:q}),(I.reactiveElementVersions??=[]).push("2.1.2");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
-const R=globalThis,q=e=>e,j=R.trustedTypes,U=j?j.createPolicy("lit-html",{createHTML:e=>e}):void 0,G="$lit$",F=`lit$${Math.random().toFixed(9).slice(2)}$`,W="?"+F,B=`<${W}>`,V=document,Q=()=>V.createComment(""),J=e=>null===e||"object"!=typeof e&&"function"!=typeof e,X=Array.isArray,K="[ \t\n\f\r]",Z=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Y=/-->/g,ee=/>/g,te=RegExp(`>|${K}(?:([^\\s"'>=/]+)(${K}*=${K}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),ne=/'/g,ie=/"/g,oe=/^(?:script|style|textarea|title)$/i,se=(e=>(t,...n)=>({_$litType$:e,strings:t,values:n}))(1),ae=Symbol.for("lit-noChange"),re=Symbol.for("lit-nothing"),le=new WeakMap,ce=V.createTreeWalker(V,129);function de(e,t){if(!X(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==U?U.createHTML(t):t}const he=(e,t)=>{const n=e.length-1,i=[];let o,s=2===t?"
":3===t?"":"",a=Z;for(let t=0;t"===l[0]?(a=o??Z,c=-1):void 0===l[1]?c=-2:(c=a.lastIndex-l[2].length,r=l[1],a=void 0===l[3]?te:'"'===l[3]?ie:ne):a===ie||a===ne?a=te:a===Y||a===ee?a=Z:(a=te,o=void 0);const h=a===te&&e[t+1].startsWith("/>")?" ":"";s+=a===Z?n+B:c>=0?(i.push(r),n.slice(0,c)+G+n.slice(c)+F+h):n+F+(-2===c?t:h)}return[de(e,s+(e[n]||">")+(2===t?" ":3===t?"":"")),i]};class pe{constructor({strings:e,_$litType$:t},n){let i;this.parts=[];let o=0,s=0;const a=e.length-1,r=this.parts,[l,c]=he(e,t);if(this.el=pe.createElement(l,n),ce.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(i=ce.nextNode())&&r.length
0){i.textContent=j?j.emptyScript:"";for(let n=0;nX(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==re&&J(this._$AH)?this._$AA.nextSibling.data=e:this.T(V.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:n}=e,i="number"==typeof n?this._$AC(e):(void 0===n.el&&(n.el=pe.createElement(de(n.h,n.h[0]),this.options)),n);if(this._$AH?._$AD===i)this._$AH.p(t);else{const e=new ge(i,this),n=e.u(this.options);e.p(t),this.T(n),this._$AH=e}}_$AC(e){let t=le.get(e.strings);return void 0===t&&le.set(e.strings,t=new pe(e)),t}k(e){X(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let n,i=0;for(const o of e)i===t.length?t.push(n=new _e(this.O(Q()),this.O(Q()),this,this.options)):n=t[i],n._$AI(o),i++;i2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=re}_$AI(e,t=this,n,i){const o=this.strings;let s=!1;if(void 0===o)e=ue(this,e,t,0),s=!J(e)||e!==this._$AH&&e!==ae,s&&(this._$AH=e);else{const i=e;let a,r;for(e=o[0],a=0;ae,W=j.trustedTypes,G=W?W.createPolicy("lit-html",{createHTML:e=>e}):void 0,V="$lit$",B=`lit$${Math.random().toFixed(9).slice(2)}$`,Q="?"+B,K=`<${Q}>`,J=document,X=()=>J.createComment(""),Z=e=>null===e||"object"!=typeof e&&"function"!=typeof e,Y=Array.isArray,ee="[ \t\n\f\r]",te=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,ne=/-->/g,ie=/>/g,se=RegExp(`>|${ee}(?:([^\\s"'>=/]+)(${ee}*=${ee}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),oe=/'/g,re=/"/g,ae=/^(?:script|style|textarea|title)$/i,le=(e=>(t,...n)=>({_$litType$:e,strings:t,values:n}))(1),ce=Symbol.for("lit-noChange"),de=Symbol.for("lit-nothing"),he=new WeakMap,pe=J.createTreeWalker(J,129);function ue(e,t){if(!Y(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==G?G.createHTML(t):t}const ge=(e,t)=>{const n=e.length-1,i=[];let s,o=2===t?"":3===t?"":"",r=te;for(let t=0;t"===l[0]?(r=s??te,c=-1):void 0===l[1]?c=-2:(c=r.lastIndex-l[2].length,a=l[1],r=void 0===l[3]?se:'"'===l[3]?re:oe):r===re||r===oe?r=se:r===ne||r===ie?r=te:(r=se,s=void 0);const h=r===se&&e[t+1].startsWith("/>")?" ":"";o+=r===te?n+K:c>=0?(i.push(a),n.slice(0,c)+V+n.slice(c)+B+h):n+B+(-2===c?t:h)}return[ue(e,o+(e[n]||">")+(2===t?" ":3===t?"":"")),i]};class _e{constructor({strings:e,_$litType$:t},n){let i;this.parts=[];let s=0,o=0;const r=e.length-1,a=this.parts,[l,c]=ge(e,t);if(this.el=_e.createElement(l,n),pe.currentNode=this.el.content,2===t||3===t){const e=this.el.content.firstChild;e.replaceWith(...e.childNodes)}for(;null!==(i=pe.nextNode())&&a.length0){i.textContent=W?W.emptyScript:"";for(let n=0;nY(e)||"function"==typeof e?.[Symbol.iterator])(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==de&&Z(this._$AH)?this._$AA.nextSibling.data=e:this.T(J.createTextNode(e)),this._$AH=e}$(e){const{values:t,_$litType$:n}=e,i="number"==typeof n?this._$AC(e):(void 0===n.el&&(n.el=_e.createElement(ue(n.h,n.h[0]),this.options)),n);if(this._$AH?._$AD===i)this._$AH.p(t);else{const e=new ve(i,this),n=e.u(this.options);e.p(t),this.T(n),this._$AH=e}}_$AC(e){let t=he.get(e.strings);return void 0===t&&he.set(e.strings,t=new _e(e)),t}k(e){Y(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let n,i=0;for(const s of e)i===t.length?t.push(n=new me(this.O(X()),this.O(X()),this,this.options)):n=t[i],n._$AI(s),i++;i2||""!==n[0]||""!==n[1]?(this._$AH=Array(n.length-1).fill(new String),this.strings=n):this._$AH=de}_$AI(e,t=this,n,i){const s=this.strings;let o=!1;if(void 0===s)e=fe(this,e,t,0),o=!Z(e)||e!==this._$AH&&e!==ce,o&&(this._$AH=e);else{const i=e;let r,a;for(e=s[0],r=0;r{const i=n?.renderBefore??t;let o=i._$litPart$;if(void 0===o){const e=n?.renderBefore??null;i._$litPart$=o=new _e(t.insertBefore(Q(),e),e,void 0,n??{})}return o._$AI(e),o})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return ae}};$e._$litElement$=!0,$e.finalized=!0,xe.litElementHydrateSupport?.({LitElement:$e});const Se=xe.litElementPolyfillSupport;Se?.({LitElement:$e}),(xe.litElementVersions??=[]).push("4.2.2");
+ */let Pe=class extends q{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){const e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){const t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=((e,t,n)=>{const i=n?.renderBefore??t;let s=i._$litPart$;if(void 0===s){const e=n?.renderBefore??null;i._$litPart$=s=new me(t.insertBefore(X(),e),e,void 0,n??{})}return s._$AI(e),s})(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return ce}};Pe._$litElement$=!0,Pe.finalized=!0,Ce.litElementHydrateSupport?.({LitElement:Pe});const ke=Ce.litElementPolyfillSupport;ke?.({LitElement:Pe}),(Ce.litElementVersions??=[]).push("4.2.2");
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
-const Ce={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:H},Ee=(e=Ce,t,n)=>{const{kind:i,metadata:o}=n;let s=globalThis.litPropertyMetadata.get(o);if(void 0===s&&globalThis.litPropertyMetadata.set(o,s=new Map),"setter"===i&&((e=Object.create(e)).wrapped=!0),s.set(n.name,e),"accessor"===i){const{name:i}=n;return{set(n){const o=t.get.call(this);t.set.call(this,n),this.requestUpdate(i,o,e,!0,n)},init(t){return void 0!==t&&this.C(i,void 0,e,t),t}}}if("setter"===i){const{name:i}=n;return function(n){const o=this[i];t.call(this,n),this.requestUpdate(i,o,e,!0,n)}}throw Error("Unsupported decorator location: "+i)};
+const Ee=e=>(t,n)=>{void 0!==n?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)},ze={attribute:!0,type:String,converter:H,reflect:!1,hasChanged:O},Ae=(e=ze,t,n)=>{const{kind:i,metadata:s}=n;let o=globalThis.litPropertyMetadata.get(s);if(void 0===o&&globalThis.litPropertyMetadata.set(s,o=new Map),"setter"===i&&((e=Object.create(e)).wrapped=!0),o.set(n.name,e),"accessor"===i){const{name:i}=n;return{set(n){const s=t.get.call(this);t.set.call(this,n),this.requestUpdate(i,s,e,!0,n)},init(t){return void 0!==t&&this.C(i,void 0,e,t),t}}}if("setter"===i){const{name:i}=n;return function(n){const s=this[i];t.call(this,n),this.requestUpdate(i,s,e,!0,n)}}throw Error("Unsupported decorator location: "+i)};
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
- */function ke(e){return(t,n)=>"object"==typeof n?Ee(e,t,n):((e,t,n)=>{const i=t.hasOwnProperty(n);return t.constructor.createProperty(n,e),i?Object.getOwnPropertyDescriptor(t,n):void 0})(e,t,n)}
+ */function Ne(e){return(t,n)=>"object"==typeof n?Ae(e,t,n):((e,t,n)=>{const i=t.hasOwnProperty(n);return t.constructor.createProperty(n,e),i?Object.getOwnPropertyDescriptor(t,n):void 0})(e,t,n)}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
- */function ze(e){return ke({...e,state:!0,attribute:!1})}
+ */function Me(e){return Ne({...e,state:!0,attribute:!1})}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
- */const Ae=2;class Pe{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,n){this._$Ct=e,this._$AM=t,this._$Ci=n}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}}
+ */const Ie=2;class Te{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,n){this._$Ct=e,this._$AM=t,this._$Ci=n}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}}
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
- */class Me extends Pe{constructor(e){if(super(e),this.it=re,e.type!==Ae)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(e){if(e===re||null==e)return this._t=void 0,this.it=e;if(e===ae)return e;if("string"!=typeof e)throw Error(this.constructor.directiveName+"() called with a non-string value");if(e===this.it)return this._t;this.it=e;const t=[e];return t.raw=t,this._t={_$litType$:this.constructor.resultType,strings:t,values:[]}}}Me.directiveName="unsafeHTML",Me.resultType=1;const Te=(e=>(...t)=>({_$litDirective$:e,values:t}))(Me),Ne={"&":"&","<":"<",">":">",'"':""","'":"'"};function De(e){return String(e).replace(/[&<>"']/g,e=>Ne[e]??e)}const Le=Object.keys(_).filter(e=>"unknown"!==e&&"always_on"!==e);class He extends HTMLElement{constructor(){super(),this.attachShadow({mode:"open"}),this._hass=null,this._config=null,this._debounceTimers={}}set hass(e){this._hass=e,this.hasAttribute("open")&&this._config&&this._updateLiveState()}get hass(){return this._hass}open(e){this._config=e,this._render(),this.offsetHeight,this.setAttribute("open","")}close(){this.removeAttribute("open"),this._config=null,this.dispatchEvent(new CustomEvent("side-panel-closed",{bubbles:!0,composed:!0}))}_render(){const e=this._config;if(!e)return;const t=this.shadowRoot;if(!t)return;t.innerHTML="";const n=document.createElement("style");n.textContent='\n :host {\n display: block;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: 360px;\n max-width: 90vw;\n z-index: 1000;\n transform: translateX(100%);\n transition: transform 0.3s ease;\n pointer-events: none;\n }\n :host([open]) {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n .backdrop {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.3);\n z-index: -1;\n }\n :host([open]) .backdrop {\n display: block;\n }\n\n .panel {\n height: 100%;\n background: var(--card-background-color, #fff);\n border-left: 1px solid var(--divider-color, #e0e0e0);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px;\n border-bottom: 1px solid var(--divider-color, #e0e0e0);\n }\n .panel-header .title {\n font-size: 18px;\n font-weight: 500;\n color: var(--primary-text-color, #212121);\n margin: 0;\n }\n .panel-header .subtitle {\n font-size: 13px;\n color: var(--secondary-text-color, #727272);\n margin: 2px 0 0 0;\n }\n .close-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color, #727272);\n padding: 4px;\n line-height: 1;\n font-size: 20px;\n }\n\n .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n }\n\n .section {\n margin-bottom: 20px;\n }\n .section-label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n color: var(--secondary-text-color, #727272);\n margin: 0 0 8px 0;\n letter-spacing: 0.5px;\n }\n\n .field-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 0;\n }\n .field-label {\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n }\n\n select {\n padding: 6px 8px;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 4px;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n font-size: 14px;\n }\n\n input[type="number"] {\n width: 72px;\n padding: 6px 8px;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 4px;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n font-size: 14px;\n text-align: right;\n }\n input[type="number"]:disabled {\n opacity: 0.5;\n }\n\n .radio-group {\n display: flex;\n gap: 16px;\n padding: 8px 0;\n }\n .radio-group label {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n cursor: pointer;\n }\n\n .horizon-bar {\n display: flex;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 6px;\n overflow: hidden;\n margin-top: 4px;\n }\n .horizon-segment {\n flex: 1;\n padding: 6px 0;\n text-align: center;\n font-size: 13px;\n cursor: pointer;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n border: none;\n border-right: 1px solid var(--divider-color, #e0e0e0);\n transition: background 0.15s ease, color 0.15s ease;\n user-select: none;\n line-height: 1.4;\n }\n .horizon-segment:last-child {\n border-right: none;\n }\n .horizon-segment:hover:not(.active) {\n background: var(--secondary-background-color, #f5f5f5);\n }\n .horizon-segment.active {\n background: var(--primary-color, #03a9f4);\n color: #fff;\n font-weight: 600;\n }\n .horizon-segment.referenced {\n box-shadow: inset 0 -3px 0 var(--primary-color, #03a9f4);\n }\n\n .monitoring-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .panel-mode-info {\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n line-height: 1.6;\n }\n .panel-mode-info p {\n margin: 0 0 12px 0;\n }\n\n .error-msg {\n color: var(--error-color, #f44336);\n font-size: 0.8em;\n padding: 8px;\n margin: 8px 0;\n background: rgba(244, 67, 54, 0.1);\n border-radius: 4px;\n }\n',t.appendChild(n);const i=document.createElement("div");i.className="backdrop",i.addEventListener("click",()=>this.close()),t.appendChild(i);const o=document.createElement("div");o.className="panel",t.appendChild(o),e.panelMode?this._renderPanelMode(o):e.subDeviceMode?this._renderSubDeviceMode(o,e):this._renderCircuitMode(o,e)}_renderPanelMode(e){const t=this._config,i=this._createHeader(n("sidepanel.graph_settings"),n("sidepanel.global_defaults"));e.appendChild(i);const a=document.createElement("div");a.className="panel-body";const r=document.createElement("div");r.className="error-msg",r.id="error-msg",r.style.display="none",a.appendChild(r);const l=t.graphSettings,c=t.topology,d=l?.global_horizon??o,h=l?.circuits??{},u=document.createElement("div");u.className="section";const g=document.createElement("div");g.className="section-label",g.textContent=n("sidepanel.graph_horizon"),u.appendChild(g);const _=document.createElement("div");_.className="field-row";const f=document.createElement("span");f.className="field-label",f.textContent=n("sidepanel.global_default"),_.appendChild(f);const m=document.createElement("select");for(const e of Object.keys(s)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,o=n(i);t.textContent=o!==i?o:e,e===d&&(t.selected=!0),m.appendChild(t)}if(m.addEventListener("change",()=>{this._callDomainService("set_graph_time_horizon",{horizon:m.value}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${e.message??e}`))}),_.appendChild(m),u.appendChild(_),a.appendChild(u),c?.circuits){const e=document.createElement("div");e.className="section";const t=document.createElement("div");t.className="section-label",t.textContent=n("sidepanel.circuit_scales"),e.appendChild(t);const i=Object.entries(c.circuits).sort(([,e],[,t])=>(e.name||"").localeCompare(t.name||""));for(const[t,o]of i){const i=document.createElement("div");i.className="field-row";const a=document.createElement("span");a.className="field-label",a.textContent=o.name||t,a.style.cssText="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;",i.appendChild(a);const r=h[t]||{horizon:d,has_override:!1},l=r.has_override?r.horizon:d,c=document.createElement("select");c.dataset.uuid=t;for(const e of Object.keys(s)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,o=n(i);t.textContent=o!==i?o:e,e===l&&(t.selected=!0),c.appendChild(t)}if(c.addEventListener("change",()=>{this._debounce(`circuit-${t}`,p,()=>{this._callDomainService("set_circuit_graph_horizon",{circuit_id:t,horizon:c.value}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${e.message??e}`))})}),i.appendChild(c),r.has_override){const e=document.createElement("button");e.textContent="↺",e.title=n("sidepanel.reset_to_global"),Object.assign(e.style,{background:"none",border:"1px solid var(--divider-color, #e0e0e0)",color:"var(--primary-text-color)",borderRadius:"4px",padding:"3px 6px",cursor:"pointer",marginLeft:"4px",fontSize:"0.85em"}),e.addEventListener("click",()=>{this._callDomainService("clear_circuit_graph_horizon",{circuit_id:t}).then(()=>{c.value=d,e.remove(),this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${e.message??e}`))}),i.appendChild(e)}e.appendChild(i)}a.appendChild(e)}const b=l?.sub_devices??{};if(c?.sub_devices){const e=document.createElement("div");e.className="section";const t=document.createElement("div");t.className="section-label",t.textContent=n("sidepanel.subdevice_scales"),e.appendChild(t);const i=Object.entries(c.sub_devices).sort(([,e],[,t])=>(e.name||"").localeCompare(t.name||""));for(const[t,o]of i){const i=document.createElement("div");i.className="field-row";const a=document.createElement("span");a.className="field-label",a.textContent=o.name||t,a.style.cssText="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;",i.appendChild(a);const r=b[t]||{horizon:d,has_override:!1},l=r.has_override?r.horizon:d,c=document.createElement("select");c.dataset.subdevId=t;for(const e of Object.keys(s)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,o=n(i);t.textContent=o!==i?o:e,e===l&&(t.selected=!0),c.appendChild(t)}if(c.addEventListener("change",()=>{this._debounce(`subdev-${t}`,p,()=>{this._callDomainService("set_subdevice_graph_horizon",{subdevice_id:t,horizon:c.value}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${e.message??e}`))})}),i.appendChild(c),r.has_override){const e=document.createElement("button");e.textContent="↺",e.title=n("sidepanel.reset_to_global"),Object.assign(e.style,{background:"none",border:"1px solid var(--divider-color, #e0e0e0)",color:"var(--primary-text-color)",borderRadius:"4px",padding:"3px 6px",cursor:"pointer",marginLeft:"4px",fontSize:"0.85em"}),e.addEventListener("click",()=>{this._callDomainService("clear_subdevice_graph_horizon",{subdevice_id:t}).then(()=>{c.value=d,e.remove(),this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${e.message??e}`))}),i.appendChild(e)}e.appendChild(i)}a.appendChild(e)}e.appendChild(a)}_renderCircuitMode(e,t){const n=`${De(String(t.breaker_rating_a))}A · ${De(String(t.voltage))}V · Tabs [${De(String(t.tabs))}]`,i=this._createHeader(De(t.name),n);e.appendChild(i);const o=document.createElement("div");o.className="panel-body",e.appendChild(o);const s=document.createElement("div");s.className="error-msg",s.id="error-msg",s.style.display="none",o.appendChild(s),this._renderRelaySection(o,t),this._renderSheddingSection(o,t),this._renderGraphHorizonSection(o,t),t.showMonitoring&&this._renderMonitoringSection(o,t)}_renderSubDeviceMode(e,t){const n=this._createHeader(De(t.name),De(t.deviceType));e.appendChild(n);const i=document.createElement("div");i.className="panel-body",e.appendChild(i);const o=document.createElement("div");o.className="error-msg",o.id="error-msg",o.style.display="none",i.appendChild(o),this._renderSubDeviceHorizonSection(i,t)}_renderSubDeviceHorizonSection(e,t){const i=document.createElement("div");i.className="section";const a=document.createElement("div");a.className="section-label",a.textContent=n("sidepanel.graph_horizon"),i.appendChild(a);const r=t.graphHorizonInfo,l=!0===r?.has_override,c=r?.horizon||o,d=r?.globalHorizon||o,h=document.createElement("div");h.className="horizon-bar";const p=[{key:"global",label:n("sidepanel.global")}];for(const e of Object.keys(s))p.push({key:e,label:e});const u=l?c:"global",g=e=>{for(const t of h.querySelectorAll(".horizon-segment")){const n=t.dataset.horizon;t.classList.toggle("active",n===e),t.classList.toggle("referenced","global"===e&&n===d)}};for(const{key:e,label:i}of p){const o=document.createElement("button");o.type="button",o.className="horizon-segment",o.dataset.horizon=e,o.textContent=i,o.classList.toggle("active",e===u),o.classList.toggle("referenced","global"===u&&e===d),o.addEventListener("click",()=>{if(o.classList.contains("active"))return;const i=t.subDeviceId;"global"===e?(g("global"),this._callDomainService("clear_subdevice_graph_horizon",{subdevice_id:i}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${n("sidepanel.clear_graph_horizon_failed")} ${e.message??e}`))):(g(e),this._callDomainService("set_subdevice_graph_horizon",{subdevice_id:i,horizon:e}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${n("sidepanel.graph_horizon_failed")} ${e.message??e}`)))}),h.appendChild(o)}i.appendChild(h),e.appendChild(i)}_createHeader(e,t){const n=document.createElement("div");n.className="panel-header";const i=document.createElement("div"),o=De(e),s=De(t);i.innerHTML=`${o}
`+(s?`${s}
`:"");const a=document.createElement("button");return a.className="close-btn",a.innerHTML="✕",a.addEventListener("click",()=>this.close()),n.appendChild(i),n.appendChild(a),n}_renderRelaySection(e,t){if(!1===t.is_user_controllable||!t.entities?.switch)return;const i=document.createElement("div");i.className="section",i.innerHTML=`${De(n("sidepanel.relay"))}
`;const o=document.createElement("div");o.className="field-row";const s=document.createElement("span");s.className="field-label",s.textContent=n("sidepanel.breaker");const a=document.createElement("ha-switch");a.dataset.role="relay-toggle";const r=t.entities.switch,l=this._hass?.states?.[r]?.state;"on"===l&&a.setAttribute("checked",""),a.addEventListener("change",()=>{const e=a.hasAttribute("checked")||a.checked;this._callService("switch",e?"turn_on":"turn_off",{entity_id:r}).catch(e=>this._showError(`${n("sidepanel.relay_failed")} ${e.message??e}`))}),o.appendChild(s),o.appendChild(a),i.appendChild(o),e.appendChild(i)}_renderSheddingSection(e,t){if(!t.entities?.select)return;const i=document.createElement("div");i.className="section",i.innerHTML=`${De(n("sidepanel.shedding_priority"))}
`;const o=document.createElement("div");o.className="field-row";const s=document.createElement("span");s.className="field-label",s.textContent=n("sidepanel.priority_label");const a=document.createElement("select");a.dataset.role="shedding-select";const r=t.entities.select,l=this._hass?.states?.[r]?.state||"";for(const e of Le){const t=_[e];if(!t)continue;const i=document.createElement("option");i.value=e,i.textContent=n(`shedding.select.${e}`)||t.label(),e===l&&(i.selected=!0),a.appendChild(i)}a.addEventListener("change",()=>{this._callService("select","select_option",{entity_id:r,option:a.value}).catch(e=>this._showError(`${n("sidepanel.shedding_failed")} ${e.message??e}`))}),o.appendChild(s),o.appendChild(a),i.appendChild(o),e.appendChild(i)}_renderGraphHorizonSection(e,t){const i=document.createElement("div");i.className="section";const a=document.createElement("div");a.className="section-label",a.textContent=n("sidepanel.graph_horizon"),i.appendChild(a);const r=t.graphHorizonInfo,l=!0===r?.has_override,c=r?.horizon||o,d=r?.globalHorizon||o,h=document.createElement("div");h.className="horizon-bar";const p=[{key:"global",label:n("sidepanel.global")}];for(const e of Object.keys(s))p.push({key:e,label:e});const u=l?c:"global",g=e=>{for(const t of h.querySelectorAll(".horizon-segment")){const n=t.dataset.horizon;t.classList.toggle("active",n===e),t.classList.toggle("referenced","global"===e&&n===d)}};for(const{key:e,label:i}of p){const o=document.createElement("button");o.type="button",o.className="horizon-segment",o.dataset.horizon=e,o.textContent=i,o.classList.toggle("active",e===u),o.classList.toggle("referenced","global"===u&&e===d),o.addEventListener("click",()=>{if(o.classList.contains("active"))return;const i=t.uuid;"global"===e?(g("global"),this._callDomainService("clear_circuit_graph_horizon",{circuit_id:i}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${n("sidepanel.clear_graph_horizon_failed")} ${e.message??e}`))):(g(e),this._callDomainService("set_circuit_graph_horizon",{circuit_id:i,horizon:e}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>this._showError(`${n("sidepanel.graph_horizon_failed")} ${e.message??e}`)))}),h.appendChild(o)}i.appendChild(h),e.appendChild(i)}_renderMonitoringSection(e,t){const i=document.createElement("div");i.className="section";const o=document.createElement("div");o.className="monitoring-header";const s=document.createElement("div");s.className="section-label",s.textContent=n("sidepanel.monitoring"),s.style.margin="0";const a=document.createElement("ha-switch");a.dataset.role="monitoring-toggle";const r=t.monitoringInfo,l=null!=r&&!1!==r.monitoring_enabled;l&&a.setAttribute("checked",""),o.appendChild(s),o.appendChild(a),i.appendChild(o);const c=document.createElement("div");c.dataset.role="monitoring-details",c.style.display=l?"block":"none",i.appendChild(c);const d=!0===r?.has_override,h=document.createElement("div");h.className="radio-group",h.innerHTML=`\n ${De(n("sidepanel.global"))} \n ${De(n("sidepanel.custom"))} \n `,c.appendChild(h);const p=document.createElement("div");p.dataset.role="threshold-fields",p.style.display=d?"block":"none";const u=r?.continuous_threshold_pct??80,g=r?.spike_threshold_pct??100,_=r?.window_duration_m??15,f=r?.cooldown_duration_m??15;p.appendChild(this._createThresholdRow(n("sidepanel.continuous_pct"),"continuous",u,t)),p.appendChild(this._createThresholdRow(n("sidepanel.spike_pct"),"spike",g,t)),p.appendChild(this._createDurationRow(n("sidepanel.window_duration"),"window-m",_,1,180,"m",t)),p.appendChild(this._createDurationRow(n("sidepanel.cooldown"),"cooldown-m",f,1,180,"m",t)),c.appendChild(p),a.addEventListener("change",()=>{const e=a.checked;c.style.display=e?"block":"none";const i=t.entities?.power||t.uuid;this._callDomainService("set_circuit_threshold",{circuit_id:i,monitoring_enabled:e}).catch(e=>this._showError(`${n("sidepanel.monitoring_toggle_failed")} ${e.message??e}`))});const m=h.querySelectorAll('input[type="radio"]');for(const e of m)e.addEventListener("change",()=>{const i="custom"===e.value&&e.checked;if(p.style.display=i?"block":"none",!i&&e.checked){const e=t.entities?.power||t.uuid;this._callDomainService("clear_circuit_threshold",{circuit_id:e}).catch(e=>this._showError(`${n("sidepanel.clear_monitoring_failed")} ${e.message??e}`))}});e.appendChild(i)}_createThresholdRow(e,t,i,o){const s=document.createElement("div");s.className="field-row";const a=document.createElement("span");a.className="field-label",a.textContent=e;const r=document.createElement("input");return r.type="number",r.min="0",r.max="200",r.value=String(i),r.dataset.role=`threshold-${t}`,r.addEventListener("input",()=>{this._debounce(`threshold-${t}`,p,()=>{const e=this.shadowRoot;if(!e)return;const t=e.querySelector('[data-role="threshold-continuous"]'),i=e.querySelector('[data-role="threshold-spike"]'),s=e.querySelector('[data-role="threshold-window-m"]'),a=e.querySelector('[data-role="threshold-cooldown-m"]'),r=o.entities?.power||o.uuid;this._callDomainService("set_circuit_threshold",{circuit_id:r,continuous_threshold_pct:t?Number(t.value):void 0,spike_threshold_pct:i?Number(i.value):void 0,window_duration_m:s?Number(s.value):void 0,cooldown_duration_m:a?Number(a.value):void 0}).catch(e=>this._showError(`${n("sidepanel.save_threshold_failed")} ${e.message??e}`))})}),s.appendChild(a),s.appendChild(r),s}_createDurationRow(e,t,i,o,s,a,r,l=!1){const c=document.createElement("div");c.className="field-row";const d=document.createElement("span");d.className="field-label",d.textContent=e;const h=document.createElement("div"),u=document.createElement("input");u.type="number",u.min=String(o),u.max=String(s),u.value=String(i),u.dataset.role=`threshold-${t}`,l&&(u.disabled=!0);const g=document.createElement("span");return g.textContent=a,h.appendChild(u),h.appendChild(g),l||u.addEventListener("input",()=>{this._debounce(`threshold-${t}`,p,()=>{const e=this.shadowRoot;if(!e)return;const t=e.querySelector('[data-role="threshold-continuous"]'),i=e.querySelector('[data-role="threshold-spike"]'),o=e.querySelector('[data-role="threshold-window-m"]');this._callDomainService("set_circuit_threshold",{circuit_id:r.uuid,continuous_threshold_pct:t?Number(t.value):void 0,spike_threshold_pct:i?Number(i.value):void 0,window_duration_m:o?Number(o.value):void 0}).catch(e=>this._showError(`${n("sidepanel.save_threshold_failed")} ${e.message??e}`))})}),c.appendChild(d),c.appendChild(h),c}_updateLiveState(){if(!this._config||this._config.panelMode)return;const e=this._config;if(!e.subDeviceMode){if(e.entities?.switch){const t=this.shadowRoot?.querySelector('[data-role="relay-toggle"]');if(t){const n=this._hass?.states?.[e.entities.switch]?.state;"on"===n?t.setAttribute("checked",""):t.removeAttribute("checked")}}if(e.entities?.select){const t=this.shadowRoot?.querySelector('[data-role="shedding-select"]');if(t){const n=this._hass?.states?.[e.entities.select]?.state||"";t.value=n}}}}_callService(e,t,n){return this._hass?Promise.resolve(this._hass.callService(e,t,n)):Promise.resolve()}_callDomainService(e,t){return this._hass?this._hass.callWS({type:"call_service",domain:a,service:e,service_data:t}):Promise.resolve()}_showError(e){const t=this.shadowRoot?.getElementById("error-msg");t&&(t.textContent=e,t.style.display="block",setTimeout(()=>{t.style.display="none"},5e3))}_debounce(e,t,n){this._debounceTimers[e]&&clearTimeout(this._debounceTimers[e]),this._debounceTimers[e]=setTimeout(()=>{delete this._debounceTimers[e],n()},t)}}try{customElements.get("span-side-panel")||customElements.define("span-side-panel",He)}catch{}async function Ie(e,t){const[n,i,o]=await Promise.all([e.callWS({type:"config/area_registry/list"}),e.callWS({type:"config/entity_registry/list"}),e.callWS({type:"config/device_registry/list"})]),s=new Map;for(const e of n)s.set(e.area_id,e.name);const a=new Map;for(const e of i)e.area_id&&a.set(e.entity_id,e.area_id);const r=new Map;for(const e of o)r.set(e.id,e.area_id);let l;if(t.device_id){const e=r.get(t.device_id);e&&(l=s.get(e))}for(const e of Object.values(t.circuits)){let t;for(const n of Object.values(e.entities)){if(!n)continue;const e=a.get(n);if(e){t=s.get(e);break}}t||(t=l),e.area=t}}async function Oe(e,t){if(!t)throw new Error(n("card.device_not_found"));const i=await e.callWS({type:`${a}/panel_topology`,device_id:t}),o=i.panel_size??function(e){let t=0;for(const n of Object.values(e))if(n)for(const e of n.tabs)e>t&&(t=e);return t>0?t+t%2:0}(i.circuits);if(!o)throw new Error(n("card.topology_error"));const s=await e.callWS({type:"config/device_registry/list"}),r=(l=s.find(e=>e.id===t),l?{id:l.id,name:l.name,name_by_user:l.name_by_user,config_entries:l.config_entries,identifiers:l.identifiers,via_device_id:l.via_device_id,sw_version:l.sw_version,model:l.model}:null);var l;return await Ie(e,i),{topology:i,panelDevice:r,panelSize:o}}const Re=u.power;function qe(e){return Re.unit(e)}function je(e){return(e<0?"-":"")+Re.format(e)}function Ue(e){return(Math.abs(e)/1e3).toFixed(1)}function Ge(e){return Math.ceil(e/2)}function Fe(e){return e%2==0?1:0}function We(e){if(2!==e.length)return null;const[t,n]=[Math.min(...e),Math.max(...e)];return Ge(t)===Ge(n)?"row-span":Fe(t)===Fe(n)?"col-span":"row-span"}function Be(e){const t=e.chart_metric??i;return u[t]??u[i]}function Ve(e,t){const n=function(e){return Be(e).entityRole}(t);return e.entities?.[n]??e.entities?.power??null}class Qe{constructor(){this._status=null,this._lastFetch=0,this._fetching=!1}async fetch(e,t){const n=Date.now();if(this._fetching)return this._status;if(this._status&&n-this._lastFetch<3e4)return this._status;this._fetching=!0;try{const i={};t&&(i.config_entry_id=t);const o=await e.callWS({type:"call_service",domain:a,service:"get_monitoring_status",service_data:i,return_response:!0});this._status=o?.response??null,this._lastFetch=n}catch{this._status=null}finally{this._fetching=!1}return this._status}invalidate(){this._lastFetch=0}get status(){return this._status}clear(){this._status=null,this._lastFetch=0}}function Je(e,t){return e?.circuits?e.circuits[t]??null:null}function Xe(e){if(!e?.utilization_pct)return"";const t=e.utilization_pct;return t>=100?"utilization-alert":t>=80?"utilization-warning":"utilization-normal"}function Ke(e,t,i,o,s,a,c,d,h,p=!1){const u=t.entities?.power,g=u?a.states[u]:null,m=g&&parseFloat(g.state)||0,b=t.device_type===l||m<0,v=t.entities?.switch,y=v?a.states[v]:null,w=y?"on"===y.state:(g?.attributes?.relay_state||t.relay_state)===r,x=t.breaker_rating_a,$=x?`${Math.round(x)}A`:"",S=De(t.name||n("grid.unknown")),C=Be(c);let E;if("current"===C.entityRole){const e=t.entities?.current,n=e?a.states[e]:null,i=n&&parseFloat(n.state)||0;E=`${C.format(i)} A `}else E=`${je(m)} ${qe(m)} `;const k=h||"unknown";let z="";if("unknown"!==k){const e=_[k]??_.unknown??{icon:"mdi:help",color:"#999",label:()=>"Unknown"};z=e.icon2?`\n \n \n `:e.textLabel?`\n \n ${e.textLabel} \n `:` `}const A=d&&function(e){return!!e&&void 0!==e.continuous_threshold_pct}(d),P=A?f:"#555",M=`\n \n `;let T="";if(null!=d?.utilization_pct){const e=d.utilization_pct;T=`${Math.round(e)}% `}const N=function(e){return!!e&&null!=e.over_threshold_since}(d);return`\n \n \n
\n ${z}\n ${T}\n ${M}\n
\n
\n
\n `}function Ze(e,t){return`\n \n — \n
\n `}const Ye={names:["power","battery power"],suffixes:["_power"]},et={names:["battery level","battery percentage"],suffixes:["_battery_level","_battery_percentage"]},tt={names:["state of energy"],suffixes:["_soe_kwh"]},nt={names:["nameplate capacity"],suffixes:["_nameplate_capacity"]};function it(e,t){if(!e.entities)return null;for(const[n,i]of Object.entries(e.entities)){if("sensor"!==i.domain)continue;const e=(i.original_name??"").toLowerCase();if(t.names.some(t=>e===t))return n;if(i.unique_id&&t.suffixes.some(e=>i.unique_id.endsWith(e)))return n}return null}function ot(e){return it(e,Ye)}function st(e){return it(e,et)}function at(e){return it(e,tt)}function rt(e){return it(e,nt)}function lt(e,t,n,i){const o=n.visible_sub_entities||{};let s="";if(!e.entities)return s;for(const[n,a]of Object.entries(e.entities)){if(i.has(n))continue;if(!0!==o[n])continue;const r=t.states[n];if(!r)continue;let l=a.original_name||r.attributes.friendly_name||n;const c=e.name||"";let d;if(l.startsWith(c+" ")&&(l=l.slice(c.length+1)),t.formatEntityState)d=t.formatEntityState(r);else{d=r.state;const e=r.attributes.unit_of_measurement||"";e&&(d+=" "+e)}if("Wh"===(r.attributes.unit_of_measurement||"")){const e=parseFloat(r.state);isNaN(e)||(d=(e/1e3).toFixed(1)+" kWh")}s+=`\n \n ${De(l)}: \n ${De(d)} \n
\n `}return s}function ct(e,t,i,o,s,a){if(i){const t=[{key:`${h}${e}_soc`,title:n("subdevice.soc"),available:!!s},{key:`${h}${e}_soe`,title:n("subdevice.soe"),available:!!a},{key:`${h}${e}_power`,title:n("subdevice.power"),available:!!o}].filter(e=>e.available);return`\n \n ${t.map(e=>`\n
\n `).join("")}\n
\n `}return o?`
`:""}function dt(e){const t=void 0!==e.history_days||void 0!==e.history_hours||void 0!==e.history_minutes,n=60*(60*(24*(t&&parseInt(String(e.history_days))||0)+(t&&parseInt(String(e.history_hours))||0))+(t?parseInt(String(e.history_minutes))||0:5))*1e3;return Math.max(n,6e4)}function ht(e){const t=s[e];return t?t.ms:s[o].ms}function pt(e){const t=e/1e3;return t<=600?Math.ceil(t):Math.min(5e3,Math.ceil(t/5))}function ut(e){return Math.max(500,Math.floor(e/5e3))}function gt(e,t,n,i,o,s){e.has(t)||e.set(t,[]);const a=e.get(t);a.push({time:i,value:n});const r=a.findIndex(e=>e.time>=o);r>0?a.splice(0,r):-1===r&&(a.length=0),a.length>s&&a.splice(0,a.length-s)}function _t(e,t,n=500){if(0===e.length)return e;e.sort((e,t)=>e.time-t.time);const i=[e[0]];for(let t=1;t=n&&i.push(e[t]);return i.length>t&&i.splice(0,i.length-t),i}async function ft(e,t,n,i,o){const s=new Date(Date.now()-i).toISOString(),a=i/36e5>72?"hour":"5minute",r=await e.callWS({type:"recorder/statistics_during_period",start_time:s,statistic_ids:t,period:a,types:["mean"]});for(const[e,t]of Object.entries(r)){const i=n.get(e);if(!i||!t)continue;const s=[];for(const e of t){const t=e.mean;if(null==t||!Number.isFinite(t))continue;const n=e.start;n>0&&s.push({time:n,value:t})}if(s.length>0){const e=o.get(i)||[],t=[...s,...e];t.sort((e,t)=>e.time-t.time),o.set(i,t)}}}async function mt(e,t,n,i,o){const s=new Date(Date.now()-i).toISOString(),a=await e.callWS({type:"history/history_during_period",start_time:s,entity_ids:t,minimal_response:!0,significant_changes_only:!0,no_attributes:!0}),r=pt(i),l=ut(i);for(const[e,t]of Object.entries(a)){const i=n.get(e);if(!i||!t)continue;const s=[];for(const e of t){const t=parseFloat(e.s);if(!Number.isFinite(t))continue;const n=1e3*(e.lu||e.lc||0);n>0&&s.push({time:n,value:t})}if(s.length>0){const e=o.get(i)||[],t=[...s,...e];o.set(i,_t(t,r,l))}}}function bt(e){if(!e.sub_devices)return[];const t=[];for(const[n,i]of Object.entries(e.sub_devices)){const e={power:ot(i)};i.type===c&&(e.soc=st(i),e.soe=at(i));for(const[i,o]of Object.entries(e))o&&t.push({entityId:o,key:`${h}${n}_${i}`,devId:n})}return t}async function vt(e,t,n,i,o,s){if(!t||!e)return;const a=new Map;for(const[e,i]of Object.entries(t.circuits)){const t=Ve(i,n);if(!t)continue;let s;s=o&&o.has(e)?ht(o.get(e)):dt(n),a.has(s)||a.set(s,{entityIds:[],uuidByEntity:new Map});const r=a.get(s);r.entityIds.push(t),r.uuidByEntity.set(t,e)}for(const{entityId:e,key:i,devId:o}of bt(t)){let t;t=s&&s.has(o)?ht(s.get(o)):dt(n),a.has(t)||a.set(t,{entityIds:[],uuidByEntity:new Map});const r=a.get(t);r.entityIds.push(e),r.uuidByEntity.set(e,i)}const r=[];for(const[t,n]of a){if(0===n.entityIds.length)continue;t>2592e5?r.push(ft(e,n.entityIds,n.uuidByEntity,t,i)):r.push(mt(e,n.entityIds,n.uuidByEntity,t,i))}await Promise.all(r)}function yt(e,t,n,o,s,a,r,l,c){const{options:d,series:h}=function(e,t,n,o,s,a=!1){n||(n=u[i]);const r=o?"140, 160, 220":"77, 217, 175",l=`rgb(${r})`,c=Date.now(),d=c-t,h=void 0!==n.fixedMin&&void 0!==n.fixedMax,p=(e??[]).filter(e=>e.time>=d).map(e=>[e.time,Math.abs(e.value)]),g=[{type:"line",data:p,showSymbol:!1,smooth:!1,...a?{}:{step:"end"},lineStyle:{width:1.5,color:l},areaStyle:{color:{type:"linear",x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:`rgba(${r}, 0.35)`},{offset:1,color:`rgba(${r}, 0.02)`}]}},itemStyle:{color:l}}],_=p.length>0?function(e){let t=0;for(const n of e)n[1]>t&&(t=n[1]);return t}(p):0,f={type:"value",splitNumber:4,axisLabel:{fontSize:10,formatter:_<10?e=>0===e?"0":e.toFixed(1):e=>n.format(e)},splitLine:{lineStyle:{opacity:.15}}};h?(f.min=n.fixedMin,f.max=n.fixedMax):_<1&&(f.min=0,f.max=1),s&&"current"===n.entityRole&&(f.min=0,f.max=Math.ceil(1.25*s),g.push({type:"line",data:[[d,.8*s],[c,.8*s]],showSymbol:!1,lineStyle:{width:1,color:"rgba(255, 200, 40, 0.6)",type:"dashed"},itemStyle:{color:"transparent"},tooltip:{show:!1}}),g.push({type:"line",data:[[d,s],[c,s]],showSymbol:!1,lineStyle:{width:1.5,color:"rgba(255, 60, 60, 0.7)",type:"solid"},itemStyle:{color:"transparent"},tooltip:{show:!1}}));const m={xAxis:{type:"time",min:d,max:c,axisLabel:{fontSize:10},splitLine:{show:!1}},yAxis:f,grid:{top:8,right:4,bottom:0,left:0,containLabel:!0},tooltip:{trigger:"axis",axisPointer:{type:"line",lineStyle:{type:"dashed"}},formatter:e=>{if(!e||0===e.length)return"";const t=e[0],i=new Date(t.value[0]).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}),o=parseFloat(t.value[1].toFixed(2));return`${i}${n.format(o)} ${n.unit(o)}
`}},animation:!1};return{options:m,series:g}}(n,o,s,a,l,c),p=r??120;e.style.minHeight=p+"px";let g=e.querySelector("ha-chart-base");g||(g=document.createElement("ha-chart-base"),g.style.display="block",g.style.width="100%",g.hass=t,e.innerHTML="",e.appendChild(g));const _=e.clientHeight;g.height=(_>0?_:p)+"px",g.hass=t,g.options=d,g.data=h}function wt(e,t,i,o,s,a){if(!e||!i||!t)return;const c=dt(o);let d=0;for(const[,e]of Object.entries(i.circuits)){const n=e.entities?.power;if(!n)continue;const i=t.states[n],o=i&&parseFloat(i.state)||0;e.device_type!==l&&(d+=Math.abs(o))}!function(e,t,n,i,o){const s="current"===(i.chart_metric||"power"),a=e.querySelector(".stat-consumption .stat-value"),r=e.querySelector(".stat-consumption .stat-unit");if(s){const e=n.panel_entities?.site_power,i=e?t.states[e]:null,o=i?parseFloat(i.attributes?.amperage):NaN;a&&(a.textContent=Number.isFinite(o)?Math.abs(o).toFixed(1):"--"),r&&(r.textContent="A")}else{const e=n.panel_entities?.site_power;if(e){const n=t.states[e];n&&(o=Math.abs(parseFloat(n.state)||0))}a&&(a.textContent=Ue(o)),r&&(r.textContent="kW")}const l=e.querySelector(".stat-upstream .stat-value"),c=e.querySelector(".stat-upstream .stat-unit");if(l){const e=n.panel_entities?.current_power,i=e?t.states[e]:null;if(s){const e=i?parseFloat(i.attributes?.amperage):NaN;l.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",c&&(c.textContent="A")}else{const e=i?Math.abs(parseFloat(i.state)||0):0;l.textContent=Ue(e),c&&(c.textContent="kW")}}const d=e.querySelector(".stat-downstream .stat-value"),h=e.querySelector(".stat-downstream .stat-unit");if(d){const e=n.panel_entities?.feedthrough_power,i=e?t.states[e]:null;if(s){const e=i?parseFloat(i.attributes?.amperage):NaN;d.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",h&&(h.textContent="A")}else{const e=i?Math.abs(parseFloat(i.state)||0):0;d.textContent=Ue(e),h&&(h.textContent="kW")}}const p=e.querySelector(".stat-solar .stat-value"),u=e.querySelector(".stat-solar .stat-unit");if(p){const e=n.panel_entities?.pv_power,i=e?t.states[e]:null;if(s){const e=i?parseFloat(i.attributes?.amperage):NaN;p.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",u&&(u.textContent="A")}else{if(i){const e=Math.abs(parseFloat(i.state)||0);p.textContent=Ue(e)}else p.textContent="--";u&&(u.textContent="kW")}}const g=e.querySelector(".stat-battery .stat-value");if(g){const e=n.panel_entities?.battery_level,i=e?t.states[e]:null;i&&(g.textContent=`${Math.round(parseFloat(i.state)||0)}`)}const _=e.querySelector(".stat-grid-state .stat-value");if(_){const e=n.panel_entities?.dsm_state,i=e?t.states[e]:null;_.textContent=i?t.formatEntityState?.(i)||i.state:"--"}}(e,t,i,o,d);const h=Be(o),p="current"===h.entityRole;for(const[o,d]of Object.entries(i.circuits)){const i=e.querySelector(`[data-uuid="${o}"]`);if(!i)continue;const u=d.entities?.power,g=u?t.states[u]:null,f=g&&parseFloat(g.state)||0,m=d.device_type===l||f<0,b=d.entities?.switch,v=b?t.states[b]:null,y=v?"on"===v.state:(g?.attributes?.relay_state||d.relay_state)===r,w=i.querySelector(".power-value");if(w)if(p){const e=d.entities?.current,n=e?t.states[e]:null,i=n&&parseFloat(n.state)||0;w.innerHTML=`${h.format(i)} A `}else w.innerHTML=`${je(f)} ${qe(f)} `;const x=i.querySelector(".toggle-pill");if(x){x.className="toggle-pill "+(y?"toggle-on":"toggle-off");const e=x.querySelector(".toggle-label");e&&(e.textContent=n(y?"grid.on":"grid.off"))}let $;if(i.classList.toggle("circuit-off",!y),i.classList.toggle("circuit-producer",m),d.always_on)$="always_on";else{const e=d.entities?.select,n=e?t.states[e]:null;$=n?n.state:"unknown"}const S=_[$]??_.unknown,C=i.querySelector(".shedding-icon");C&&(C.setAttribute("icon",S.icon),C.style.color=S.color,C.title=S.label());const E=i.querySelector(".shedding-icon-secondary");E&&(S.icon2?(E.setAttribute("icon",S.icon2),E.style.color=S.color,E.style.display=""):E.style.display="none");const k=i.querySelector(".shedding-label");k&&(S.textLabel?(k.textContent=S.textLabel,k.style.color=S.color,k.style.display=""):k.style.display="none");const z=i.querySelector(".chart-container");if(z){const e=s.get(o)||[],n=i.classList.contains("circuit-col-span")?200:100,r=a?.has(o)?ht(a.get(o)):c,p=d.device_type===l;yt(z,t,e,r,h,m,n,d.breaker_rating_a??void 0,p)}}}class xt{constructor(){this._settings=null,this._lastFetch=0,this._fetching=!1}async fetch(e,t){const n=Date.now();if(this._fetching)return this._settings;if(this._settings&&n-this._lastFetch<3e4)return this._settings;this._fetching=!0;try{const i={};t&&(i.config_entry_id=t);const o=await e.callWS({type:"call_service",domain:a,service:"get_graph_settings",service_data:i,return_response:!0});this._settings=o?.response??null,this._lastFetch=n}catch{this._settings=null}finally{this._fetching=!1}return this._settings}invalidate(){this._lastFetch=0}get settings(){return this._settings}clear(){this._settings=null,this._lastFetch=0}}function $t(e,t){if(!e)return o;const n=e.circuits?.[t];return n?.has_override?n.horizon:e.global_horizon??o}function St(e,t){if(!e)return o;const n=e.sub_devices?.[t];return n?.has_override?n.horizon:e.global_horizon??o}class Ct{constructor(){this.powerHistory=new Map,this.horizonMap=new Map,this.subDeviceHorizonMap=new Map,this.monitoringCache=new Qe,this.graphSettingsCache=new xt,this._hass=null,this._topology=null,this._config=null,this._configEntryId=null,this._showMonitoring=!1,this._updateInterval=null,this._recorderRefreshInterval=null,this._resizeObserver=null,this._lastWidth=0,this._resizeDebounce=null}get hass(){return this._hass}set hass(e){this._hass=e}get topology(){return this._topology}get config(){return this._config}set showMonitoring(e){this._showMonitoring=e}init(e,t,n,i){this._topology=e,this._config=t,this._hass=n,this._configEntryId=i}setConfig(e){this._config=e}buildHorizonMaps(e){if(this.horizonMap.clear(),this.subDeviceHorizonMap.clear(),e&&this._topology?.circuits)for(const t of Object.keys(this._topology.circuits))this.horizonMap.set(t,$t(e,t));if(e&&this._topology?.sub_devices)for(const t of Object.keys(this._topology.sub_devices))this.subDeviceHorizonMap.set(t,St(e,t))}async fetchAndBuildHorizonMaps(){try{await this.graphSettingsCache.fetch(this._hass,this._configEntryId),this.buildHorizonMaps(this.graphSettingsCache.settings)}catch{}}async loadHistory(){await vt(this._hass,this._topology,this._config,this.powerHistory,this.horizonMap,this.subDeviceHorizonMap)}recordSamples(){if(!this._topology||!this._hass||!this._config)return;const e=Date.now();for(const[t,n]of Object.entries(this._topology.circuits)){const i=this.horizonMap.get(t)??o;if(!s[i]?.useRealtime)continue;const a=Ve(n,this._config);if(!a)continue;const r=this._hass.states[a];if(!r)continue;const l=parseFloat(r.state);if(isNaN(l))continue;const c=ht(i),d=pt(c),h=ut(c),p=e-c,u=this.powerHistory.get(t)??[];u.length>0&&e-u[u.length-1].time0&&e-u[u.length-1].time0&&this._topology)for(const{key:e,devId:t}of bt(this._topology))n.has(t)&&i.add(e);const o=new Map;try{await vt(this._hass,this._topology,this._config,o,t,n);for(const e of t.keys()){const t=o.get(e);t?this.powerHistory.set(e,t):this.powerHistory.delete(e)}for(const e of i){const t=o.get(e);t?this.powerHistory.set(e,t):this.powerHistory.delete(e)}this.updateDOM(e)}catch{}}updateDOM(e){this._hass&&this._topology&&this._config&&(wt(e,this._hass,this._topology,this._config,this.powerHistory,this.horizonMap),function(e,t,n,i,o,s){if(!n.sub_devices)return;const a=dt(i);for(const[i,r]of Object.entries(n.sub_devices)){const n=e.querySelector(`[data-subdev="${i}"]`);if(!n)continue;const l=ot(r);if(l){const e=t.states[l],i=e&&parseFloat(e.state)||0,o=n.querySelector(".sub-power-value");o&&(o.innerHTML=`${je(i)} ${qe(i)} `)}const c=n.querySelectorAll("[data-chart-key]");for(const e of c){const n=e.dataset.chartKey;if(!n)continue;const r=o.get(n)||[];let l=g.power;n.endsWith("_soc")?l=g.soc:n.endsWith("_soe")&&(l=g.soe);const c=!!e.closest(".bess-chart-col");yt(e,t,r,s?.has(i)?ht(s.get(i)):a,l,!1,c?120:150,void 0,n.endsWith("_soc")||n.endsWith("_soe"))}for(const e of Object.keys(r.entities||{})){const i=n.querySelector(`[data-eid="${e}"]`);if(!i)continue;const o=t.states[e];if(o){let e;if(t.formatEntityState)e=t.formatEntityState(o);else{e=o.state;const t=o.attributes.unit_of_measurement||"";t&&(e+=" "+t)}if("Wh"===(o.attributes.unit_of_measurement||"")){const t=parseFloat(o.state);isNaN(t)||(e=(t/1e3).toFixed(1)+" kWh")}i.textContent=e}}}}(e,this._hass,this._topology,this._config,this.powerHistory,this.subDeviceHorizonMap))}async onGraphSettingsChanged(e){if(this._hass){this.graphSettingsCache.invalidate(),await this.graphSettingsCache.fetch(this._hass,this._configEntryId),this.buildHorizonMaps(this.graphSettingsCache.settings),this.powerHistory.clear();try{await this.loadHistory()}catch{}this.updateDOM(e)}}onToggleClick(e,t){const n=e.target,i=n?.closest(".toggle-pill");if(!i)return;const o=t.querySelector(".slide-confirm");if(!o||!o.classList.contains("confirmed"))return;e.stopPropagation(),e.preventDefault();const s=i.closest("[data-uuid]");if(!s||!this._topology||!this._hass)return;const a=s.dataset.uuid;if(!a)return;const r=this._topology.circuits[a];if(!r)return;const l=r.entities?.switch;if(!l)return;const c=this._hass.states[l];if(!c)return void console.warn("SPAN Panel: switch entity not found:",l);const d="on"===c.state?"turn_off":"turn_on";this._hass.callService("switch",d,{},{entity_id:l}).catch(e=>{console.error("SPAN Panel: switch service call failed:",e)})}async onGearClick(e,t){const n=e.target,i=n?.closest(".gear-icon");if(!i)return;const s=t.querySelector("span-side-panel");if(!s||!this._hass)return;if(s.hass=this._hass,i.classList.contains("panel-gear"))return await this.graphSettingsCache.fetch(this._hass,this._configEntryId),void s.open({panelMode:!0,topology:this._topology,graphSettings:this.graphSettingsCache.settings});const a=i.dataset.uuid;if(a&&this._topology){const e=this._topology.circuits[a];if(e){await this.monitoringCache.fetch(this._hass,this._configEntryId);const t=e.entities?.current??e.entities?.power,n=t?this.monitoringCache.status?.circuits?.[t]??null:null;await this.graphSettingsCache.fetch(this._hass,this._configEntryId);const i=this.graphSettingsCache.settings,r=i?.global_horizon??o,l=i?.circuits?.[a],c=l?{...l,globalHorizon:r}:{horizon:r,has_override:!1,globalHorizon:r};return void s.open({...e,uuid:a,monitoringInfo:n,showMonitoring:this._showMonitoring,graphHorizonInfo:c})}}const r=i.dataset.subdevId;if(r&&this._topology?.sub_devices?.[r]){const e=this._topology.sub_devices[r];await this.graphSettingsCache.fetch(this._hass,this._configEntryId);const t=this.graphSettingsCache.settings,n=t?.global_horizon??o,i=t?.sub_devices?.[r],a=i?{...i,globalHorizon:n}:{horizon:n,has_override:!1,globalHorizon:n};s.open({subDeviceMode:!0,subDeviceId:r,name:e.name??r,deviceType:e.type??"",graphHorizonInfo:a})}}bindSlideConfirm(e,t){const n=e.querySelector(".slide-confirm-knob"),i=e.querySelector(".slide-confirm-text");if(!n||!i)return;let o=!1,s=0,a=0;const r=t=>{e.classList.contains("confirmed")||(o=!0,s=t-n.offsetLeft,a=e.offsetWidth-n.offsetWidth-4,n.classList.remove("snapping"))},l=e=>{if(!o)return;const t=Math.max(2,Math.min(e-s,a));n.style.left=t+"px"},c=()=>{if(!o)return;o=!1;(n.offsetLeft-2)/a>=.9?(n.style.left=a+"px",e.classList.add("confirmed"),n.querySelector("ha-icon")?.setAttribute("icon","mdi:lock-open"),i.textContent=e.dataset.textOn??"",t&&t.classList.remove("switches-disabled")):(n.classList.add("snapping"),n.style.left="2px")};n.addEventListener("mousedown",e=>{e.preventDefault(),r(e.clientX)}),e.addEventListener("mousemove",e=>l(e.clientX)),e.addEventListener("mouseup",c),e.addEventListener("mouseleave",c),n.addEventListener("touchstart",e=>{e.preventDefault(),r(e.touches[0].clientX)},{passive:!1}),e.addEventListener("touchmove",e=>l(e.touches[0].clientX),{passive:!0}),e.addEventListener("touchend",c),e.addEventListener("touchcancel",c),e.addEventListener("click",()=>{e.classList.contains("confirmed")&&(e.classList.remove("confirmed"),n.classList.add("snapping"),n.style.left="2px",n.querySelector("ha-icon")?.setAttribute("icon","mdi:lock"),i.textContent=e.dataset.textOff??"",t&&t.classList.add("switches-disabled"))})}startIntervals(e,t){this._updateInterval=setInterval(()=>{this.recordSamples(),this.updateDOM(e),t&&t()},1e3),this._recorderRefreshInterval=setInterval(()=>{this.refreshRecorderData(e)},3e4)}stopIntervals(){this._updateInterval&&(clearInterval(this._updateInterval),this._updateInterval=null),this._recorderRefreshInterval&&(clearInterval(this._recorderRefreshInterval),this._recorderRefreshInterval=null),this.cleanupResizeObserver()}setupResizeObserver(e,t){this.cleanupResizeObserver(),t&&(this._lastWidth=t.clientWidth,this._resizeObserver=new ResizeObserver(t=>{const n=t[0];if(!n)return;const i=n.contentRect.width;Math.abs(i-this._lastWidth)<5||(this._lastWidth=i,this._resizeDebounce&&clearTimeout(this._resizeDebounce),this._resizeDebounce=setTimeout(()=>{for(const t of e.querySelectorAll(".chart-container")){const e=t.querySelector("ha-chart-base");e&&e.remove()}this.updateDOM(e)},150))}),this._resizeObserver.observe(t))}cleanupResizeObserver(){this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._resizeDebounce&&(clearTimeout(this._resizeDebounce),this._resizeDebounce=null)}reset(){this.powerHistory.clear(),this.horizonMap.clear(),this.subDeviceHorizonMap.clear(),this.monitoringCache.clear(),this.graphSettingsCache.clear()}}const Et="\n :host {\n --span-accent: var(--primary-color, #4dd9af);\n }\n\n ha-card {\n padding: 24px;\n background: var(--card-background-color, #1c1c1c);\n color: var(--primary-text-color, #e0e0e0);\n border-radius: var(--ha-card-border-radius, 12px);\n border: var(--ha-card-border-width, 1px) solid var(--ha-card-border-color, var(--divider-color, #333));\n box-shadow: var(--ha-card-box-shadow, none);\n }\n\n .panel-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px 16px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .header-left { flex: 1 1 300px; min-width: 0; }\n .header-center { flex: 0 0 auto; }\n .header-right { flex: 0 1 auto; min-width: 0; }\n\n .panel-identity {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px 12px;\n margin-bottom: 12px;\n }\n\n .panel-title {\n font-size: 1.8em;\n font-weight: 700;\n margin: 0;\n color: var(--primary-text-color, #fff);\n }\n\n .panel-serial {\n font-size: 0.85em;\n color: var(--secondary-text-color, #999);\n font-family: monospace;\n }\n\n .panel-stats {\n display: flex;\n flex-wrap: wrap;\n gap: 16px 32px;\n }\n\n .stat { display: flex; flex-direction: column; }\n .stat-label { font-size: 0.8em; color: var(--secondary-text-color, #999); margin-bottom: 2px; }\n .stat-row { display: flex; align-items: baseline; gap: 2px; }\n .stat-value { font-size: 1.5em; font-weight: 700; color: var(--primary-text-color, #fff); }\n .stat-unit { font-size: 0.7em; font-weight: 400; color: var(--secondary-text-color, #999); }\n\n .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; padding-top: 8px; }\n .header-right-top { display: flex; gap: 20px; align-items: center; }\n .meta-item { font-size: 0.8em; color: var(--secondary-text-color, #999); }\n\n .shedding-legend { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }\n .shedding-legend-item { display: inline-flex; align-items: center; gap: 3px; }\n .shedding-legend-item ha-icon { --mdc-icon-size: 16px; }\n .shedding-legend-secondary { --mdc-icon-size: 12px; opacity: 0.8; }\n .shedding-legend-text { font-size: 9px; font-weight: 600; }\n .shedding-legend-label { font-size: 0.7em; color: var(--secondary-text-color, #999); }\n\n .panel-gear {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color);\n opacity: 0.6;\n padding: 4px;\n margin-left: 8px;\n vertical-align: middle;\n }\n .panel-gear:hover { opacity: 1; }\n .header-center {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding-top: 8px;\n }\n .panel-identity .panel-gear {\n margin-left: 0;\n }\n .slide-confirm {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 160px;\n height: 28px;\n border-radius: 14px;\n background: color-mix(in srgb, var(--primary-color, #4dd9af) 20%, var(--secondary-background-color, #333));\n vertical-align: middle;\n overflow: hidden;\n user-select: none;\n touch-action: none;\n }\n .slide-confirm-text {\n position: absolute;\n width: 100%;\n text-align: center;\n font-size: 0.65em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n pointer-events: none;\n z-index: 0;\n }\n .slide-confirm-knob {\n position: absolute;\n left: 2px;\n top: 2px;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--secondary-text-color, #666);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: grab;\n z-index: 1;\n transition: none;\n }\n .slide-confirm-knob ha-icon {\n --mdc-icon-size: 14px;\n color: var(--card-background-color, #1c1c1c);\n }\n .slide-confirm-knob.snapping {\n transition: left 0.25s ease;\n }\n .slide-confirm.confirmed {\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n }\n .slide-confirm.confirmed .slide-confirm-text {\n color: var(--state-active-color, var(--span-accent));\n }\n .slide-confirm.confirmed .slide-confirm-knob {\n background: var(--state-active-color, var(--span-accent));\n }\n .switches-disabled .toggle-pill {\n opacity: 0.3;\n pointer-events: none;\n }\n .unit-toggle {\n display: inline-flex;\n background: var(--secondary-background-color, #333);\n border-radius: 6px;\n overflow: hidden;\n margin-left: 8px;\n }\n .unit-btn {\n padding: 4px 10px;\n border: none;\n background: none;\n color: var(--secondary-text-color);\n font-size: 0.75em;\n font-weight: 600;\n cursor: pointer;\n }\n .unit-btn.unit-active {\n background: var(--primary-color, #4dd9af);\n color: var(--text-primary-color, #000);\n }\n\n .monitoring-summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 16px;\n font-size: 0.8em;\n background: rgba(76, 175, 80, 0.1);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n }\n .monitoring-active { color: #4caf50; }\n .monitoring-counts { display: flex; gap: 12px; }\n .count-warning { color: #ff9800; }\n .count-alert { color: #f44336; }\n .count-overrides { color: var(--secondary-text-color); }\n\n .panel-grid {\n display: grid;\n grid-template-columns: 28px 1fr 1fr 28px;\n gap: 8px;\n align-items: stretch;\n }\n\n .tab-label {\n display: flex;\n align-items: center;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n user-select: none;\n }\n .tab-left { justify-content: flex-start; }\n .tab-right { justify-content: flex-end; }\n\n .circuit-slot {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px 20px;\n min-height: 140px;\n transition: opacity 0.3s;\n position: relative;\n overflow: hidden;\n }\n\n .circuit-col-span { min-height: 280px; }\n .circuit-row-span { border-left: 3px solid var(--span-accent); }\n .circuit-off .circuit-name,\n .circuit-off .breaker-badge,\n .circuit-off .power-value,\n .circuit-off .chart-container { opacity: 0.35; }\n .circuit-off .toggle-pill,\n .circuit-off .gear-icon { opacity: 1; }\n\n .circuit-empty {\n opacity: 0.2;\n min-height: 60px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-style: dashed;\n }\n .empty-label { color: var(--secondary-text-color, #999); font-size: 0.85em; }\n\n .circuit-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 6px;\n gap: 8px;\n }\n\n .circuit-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }\n\n .breaker-badge {\n background: color-mix(in srgb, var(--span-accent) 15%, transparent);\n color: var(--span-accent);\n font-size: 0.7em;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 4px;\n white-space: nowrap;\n border: 1px solid color-mix(in srgb, var(--span-accent) 25%, transparent);\n flex-shrink: 0;\n }\n\n .circuit-name {\n font-size: 0.9em;\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--primary-text-color, #e0e0e0);\n }\n\n .circuit-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n\n .power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .power-value strong { font-weight: 700; font-size: 1.1em; }\n .power-unit { font-size: 0.8em; font-weight: 400; color: var(--secondary-text-color, #999); margin-left: 1px; }\n .circuit-producer .power-value strong { color: var(--info-color, #4fc3f7); }\n\n .toggle-pill {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 4px;\n border-radius: 10px;\n cursor: pointer;\n font-size: 0.65em;\n font-weight: 600;\n transition: background 0.2s;\n user-select: none;\n min-width: 40px;\n }\n .toggle-on {\n padding-left: 6px;\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n color: var(--state-active-color, var(--span-accent));\n }\n .toggle-off {\n padding-right: 6px;\n background: color-mix(in srgb, var(--secondary-text-color) 15%, transparent);\n color: var(--secondary-text-color, #999);\n }\n .toggle-knob {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n transition: background 0.2s, margin 0.2s;\n }\n .toggle-on .toggle-knob {\n background: var(--state-active-color, var(--span-accent));\n margin-left: auto;\n }\n .toggle-off .toggle-knob {\n background: var(--secondary-text-color, #999);\n margin-right: auto;\n order: -1;\n }\n\n .circuit-status {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-top: 4px;\n padding: 0 4px;\n }\n .shedding-icon { opacity: 0.8; cursor: default; }\n .shedding-composite {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n }\n .shedding-icon-secondary { opacity: 0.8; }\n .shedding-label {\n font-size: 10px;\n font-weight: 600;\n opacity: 0.8;\n }\n .gear-icon {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n opacity: 0.6;\n transition: opacity 0.2s;\n margin-left: auto;\n }\n .gear-icon:hover { opacity: 1; }\n .utilization {\n font-size: 0.75em;\n font-weight: 600;\n }\n .utilization-normal { color: #4caf50; }\n .utilization-warning { color: #ff9800; }\n .utilization-alert { color: #f44336; }\n .circuit-alert {\n border-color: #f44336 !important;\n box-shadow: 0 0 8px rgba(244, 67, 54, 0.3);\n }\n .circuit-custom-monitoring {\n border-left: 3px solid #ff9800;\n }\n\n .chart-container {\n width: 100%;\n aspect-ratio: 4 / 1;\n margin-top: 4px;\n overflow: hidden;\n min-width: 0;\n }\n\n .sub-devices {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 12px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .sub-device {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px;\n }\n .sub-device-bess,\n .sub-device-full {\n grid-column: 1 / -1;\n }\n\n .sub-device-header { display: flex; gap: 10px; align-items: baseline; margin-bottom: 8px; }\n .sub-device-type { font-size: 0.7em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--span-accent); }\n .sub-device-name { font-size: 0.85em; color: var(--secondary-text-color, #999); flex: 1; }\n .sub-power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .sub-power-value strong { font-weight: 700; font-size: 1.1em; }\n .sub-device .chart-container { margin-bottom: 8px; aspect-ratio: auto; }\n\n .bess-charts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(0, 1fr));\n gap: 12px;\n margin-bottom: 10px;\n }\n .bess-chart-col { min-width: 0; }\n .bess-chart-title {\n font-size: 0.75em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--secondary-text-color, #999);\n margin-bottom: 4px;\n }\n .bess-chart-col .chart-container { aspect-ratio: auto; }\n .sub-entity { display: flex; gap: 6px; padding: 3px 0; font-size: 0.85em; }\n .sub-entity-name { color: var(--secondary-text-color, #999); }\n .sub-entity-value { font-weight: 500; color: var(--primary-text-color, #e0e0e0); }\n\n /* ── Shared tab bar ────────────────────────────────────── */\n\n .shared-tab-bar {\n display: flex;\n gap: 0;\n margin-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .shared-tab {\n padding: 8px 16px;\n cursor: pointer;\n font-size: 0.9em;\n font-weight: 500;\n color: var(--primary-text-color);\n opacity: 0.6;\n border: none;\n border-bottom: 2px solid transparent;\n background: none;\n transition: opacity 0.15s;\n }\n\n .shared-tab:hover {\n opacity: 0.85;\n }\n\n .shared-tab.active {\n opacity: 1;\n border-bottom-color: var(--span-accent);\n }\n\n /* ── List view search ──────────────────────────────────── */\n\n .list-search-container {\n margin-bottom: 12px;\n position: relative;\n }\n\n .list-search {\n width: 100%;\n padding: 8px 36px 8px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--secondary-background-color, #2a2a2a);\n color: var(--primary-text-color);\n font-size: 0.9em;\n box-sizing: border-box;\n outline: none;\n }\n\n .list-search:focus {\n border-color: var(--span-accent);\n }\n\n .list-search-clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 2px;\n display: flex;\n align-items: center;\n opacity: 0.7;\n }\n\n .list-search-clear:hover {\n opacity: 1;\n }\n\n .list-unit-toggle {\n display: inline-flex;\n margin-bottom: 12px;\n }\n\n /* ── List rows ─────────────────────────────────────────── */\n\n .list-view {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .list-row {\n display: flex;\n align-items: center;\n padding: 12px 16px;\n gap: 10px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n }\n\n .list-row:hover {\n background: var(--secondary-background-color, #2a2a2a);\n }\n\n .list-row.circuit-off {\n opacity: 0.5;\n }\n\n .list-row.list-row-expanded {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-bottom-color: transparent;\n }\n\n .list-circuit-name {\n flex: 1;\n color: var(--primary-text-color);\n font-size: 0.9em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .list-status-badge {\n font-size: 0.75em;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n flex-shrink: 0;\n }\n\n .list-status-on {\n color: #4dd9af;\n }\n\n .list-status-off {\n color: #f44336;\n }\n\n .list-power-value {\n font-size: 0.9em;\n font-weight: 600;\n min-width: 70px;\n text-align: right;\n flex-shrink: 0;\n }\n\n .list-expand-toggle {\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 4px;\n transition: transform 0.2s;\n display: flex;\n align-items: center;\n flex-shrink: 0;\n }\n\n .list-expand-toggle.expanded {\n transform: rotate(180deg);\n }\n\n /* ── Expanded circuit content ──────────────────────────── */\n\n .list-expanded-content {\n padding: 12px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n border-radius: 0 0 8px 8px;\n margin-top: -6px;\n margin-bottom: 2px;\n }\n\n .list-expanded-content .circuit-slot {\n border: none;\n margin: 0;\n background: none;\n }\n\n /* ── Area headers ──────────────────────────────────────── */\n\n .area-header {\n padding: 16px 12px 6px;\n font-weight: 600;\n font-size: 0.85em;\n color: var(--secondary-text-color);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n /* ── No results ────────────────────────────────────────── */\n\n .list-no-results {\n padding: 24px;\n text-align: center;\n color: var(--secondary-text-color);\n }\n\n";class kt{constructor(){this._ctrl=new Ct,this._container=null,this._onGearClick=null,this._onToggleClick=null,this._onSidePanelClosed=null,this._onGraphSettingsChanged=null}get hass(){return this._ctrl.hass}set hass(e){this._ctrl.hass=e}async render(e,t,i,o,s){let a,r;this.stop(),this._ctrl.reset(),this._ctrl.showMonitoring=!0,this._container=e,this._ctrl.hass=t;try{const e=await Oe(t,i);a=e.topology,r=e.panelSize}catch(t){return void(e.innerHTML=`${De(t.message)}
`)}this._ctrl.init(a,o,t,s??null),await this._ctrl.monitoringCache.fetch(t,s??null),await this._ctrl.fetchAndBuildHorizonMaps();const l=Math.ceil(r/2),h=this._ctrl.monitoringCache.status,p=function(e,t){const i=De(e.device_name||n("header.default_name")),o=De(e.serial||""),s=De(e.firmware||""),a="current"===(t.chart_metric||"power"),r=!!e.panel_entities?.site_power,l=!!e.panel_entities?.dsm_state,c=!!e.panel_entities?.current_power,d=!!e.panel_entities?.feedthrough_power,h=!!e.panel_entities?.pv_power,p=!!e.panel_entities?.battery_level;return`\n \n `}(a,o),u=function(e){if(!e)return"";const t=Object.values(e.circuits??{}),i=Object.values(e.mains??{}),o=[...t,...i],s=o.filter(e=>void 0!==e.utilization_pct&&e.utilization_pct>=80&&e.utilization_pct<100).length,a=o.filter(e=>void 0!==e.utilization_pct&&e.utilization_pct>=100).length,r=o.filter(e=>e.has_override).length;return`\n \n ✓ ${n("status.monitoring")} · ${t.length} ${n("status.circuits")} · ${i.length} ${n("status.mains")} \n \n ${s>0?`${s} ${n(s>1?"status.warnings":"status.warning")} `:""}\n ${a>0?`${a} ${n(a>1?"status.alerts":"status.alert")} `:""}\n ${r>0?`${r} ${n(r>1?"status.overrides":"status.override")} `:""}\n \n
\n `}(h),g=function(e,t,n,i,o){const s=new Map,a=new Set;for(const[t,n]of Object.entries(e.circuits)){const e=n.tabs;if(!e||0===e.length)continue;const i=Math.min(...e),o=1===e.length?"single":We(e)??"single";s.set(i,{uuid:t,circuit:n,layout:o});for(const t of e)a.add(t)}const r=new Set,l=new Set;for(const[e,t]of s)if("col-span"===t.layout){const n=t.circuit.tabs,i=Ge(Math.max(...n));0===Fe(e)?r.add(i):l.add(i)}function c(e){const t=e.circuit.entities?.current??e.circuit.entities?.power,i=o?Je(o,t??""):null;let s;if(e.circuit.always_on)s="always_on";else{const t=e.circuit.entities?.select;s=t&&n.states[t]?n.states[t].state:"unknown"}return{monInfo:i,sheddingPriority:s}}let d="";for(let e=1;e<=t;e++){const t=2*e-1,o=2*e,h=s.get(t),p=s.get(o);if(d+=`${t}
`,h&&"row-span"===h.layout){const{monInfo:t,sheddingPriority:s}=c(h);d+=Ke(h.uuid,h.circuit,e,"2 / 4","row-span",n,i,t,s),d+=`${o}
`;continue}if(!r.has(e))if(!h||"col-span"!==h.layout&&"single"!==h.layout)a.has(t)||(d+=Ze(e,"2"));else{const{monInfo:t,sheddingPriority:o}=c(h);d+=Ke(h.uuid,h.circuit,e,"2",h.layout,n,i,t,o)}if(!l.has(e))if(!p||"col-span"!==p.layout&&"single"!==p.layout)a.has(o)||(d+=Ze(e,"3"));else{const{monInfo:t,sheddingPriority:o}=c(p);d+=Ke(p.uuid,p.circuit,e,"3",p.layout,n,i,t,o)}d+=`${o}
`}return d}(a,l,t,o,h),f=function(e,t,i){const o=!1!==i.show_battery,s=!1!==i.show_evse;if(!e.sub_devices)return"";const a=Object.entries(e.sub_devices).filter(([,e])=>!(e.type===c&&!o||e.type===d&&!s));if(0===a.length)return"";const r=a.filter(([,e])=>e.type===d).length;let l=0,h="";for(const[e,o]of a){const s=o.type===d?n("subdevice.ev_charger"):o.type===c?n("subdevice.battery"):n("subdevice.fallback"),a=ot(o),p=a?t.states[a]:void 0,u=p&&parseFloat(p.state)||0,g=o.type===c,_=o.type===d,f=g?st(o):null,m=g?at(o):null,b=g?rt(o):null,v=lt(o,t,i,new Set([a,f,m,b].filter(e=>null!==e))),y=ct(e,0,g,a,f,m);let w="";g?w="sub-device-bess":_&&(l++,l===r&&r%2==1&&(w="sub-device-full")),h+=`\n \n \n ${y}\n ${v}\n
\n `}return h}(a,t,o);e.innerHTML=`\n \n ${p}\n ${u}\n ${f?`${f}
`:""}\n ${!1!==o.show_panel?`\n \n ${g}\n
\n `:""}\n \n `,this._onGearClick=t=>{this._ctrl.onGearClick(t,e)},this._onToggleClick=t=>{this._ctrl.onToggleClick(t,e)},e.addEventListener("click",this._onGearClick),e.addEventListener("click",this._onToggleClick),this._onSidePanelClosed=()=>{this._ctrl.monitoringCache.invalidate(),this._ctrl.graphSettingsCache.invalidate()},e.addEventListener("side-panel-closed",this._onSidePanelClosed),this._onGraphSettingsChanged=()=>this._ctrl.onGraphSettingsChanged(e),e.addEventListener("graph-settings-changed",this._onGraphSettingsChanged);try{await this._ctrl.loadHistory()}catch{}this._ctrl.updateDOM(e);const m=e.querySelector(".slide-confirm");m&&(this._ctrl.bindSlideConfirm(m,e),e.classList.add("switches-disabled")),this._ctrl.setupResizeObserver(e,e),this._ctrl.startIntervals(e)}stop(){this._ctrl.stopIntervals(),this._container&&(this._onGearClick&&(this._container.removeEventListener("click",this._onGearClick),this._onGearClick=null),this._onToggleClick&&(this._container.removeEventListener("click",this._onToggleClick),this._onToggleClick=null),this._onSidePanelClosed&&(this._container.removeEventListener("side-panel-closed",this._onSidePanelClosed),this._onSidePanelClosed=null),this._onGraphSettingsChanged&&(this._container.removeEventListener("graph-settings-changed",this._onGraphSettingsChanged),this._onGraphSettingsChanged=null))}}const zt="\n display:flex;align-items:center;gap:8px;margin-bottom:8px;\n",At="\n background:var(--secondary-background-color,#333);\n border:1px solid var(--divider-color);\n color:var(--primary-text-color);\n border-radius:4px;padding:6px 10px;width:80px;font-size:0.85em;\n",Pt="\n min-width:130px;font-size:0.85em;color:var(--secondary-text-color);\n",Mt="\n min-width:160px;font-size:0.85em;color:var(--secondary-text-color);\n",Tt="\n background:var(--secondary-background-color,#333);\n border:1px solid var(--divider-color);\n color:var(--primary-text-color);\n border-radius:4px;padding:6px 10px;flex:1;font-size:0.85em;\n font-family:monospace;\n";function Nt(e,t,n,i,o){return`\n ${i} \n `}class Dt{constructor(){this._debounceTimer=null,this._configEntryId=null,this._notifyCloseHandler=null}stop(){this._notifyCloseHandler&&(document.removeEventListener("click",this._notifyCloseHandler),this._notifyCloseHandler=null),this._debounceTimer&&(clearTimeout(this._debounceTimer),this._debounceTimer=null)}async render(e,t,i){let o;void 0!==i&&(this._configEntryId=i),this._notifyCloseHandler&&(document.removeEventListener("click",this._notifyCloseHandler),this._notifyCloseHandler=null);try{const e={};this._configEntryId&&(e.config_entry_id=this._configEntryId);const n=await t.callWS({type:"call_service",domain:a,service:"get_monitoring_status",service_data:e,return_response:!0});o=n?.response||null}catch{o=null}const s=o?.global_settings??{},r=!0===o?.enabled,l=o?.circuits??{},c=o?.mains??{},d=new Set;for(const e of Object.keys(t.states||{}))e.startsWith("notify.")&&d.add(e);const h=new Set(["notify","send_message"]);for(const e of Object.keys(t.services?.notify||{}))h.has(e)||d.add(`notify.${e}`);d.add("event_bus");const p=[...d].sort(),u=s.notify_targets??"",g=("string"==typeof u?u.split(","):u).map(e=>e.trim()).filter(Boolean),_=p.length>0&&p.every(e=>g.includes(e)),f=s.notification_title_template??"SPAN: {name} {alert_type}",m=s.notification_message_template??"{name} at {current_a}A ({utilization_pct}% of {breaker_rating_a}A rating)",b=s.notification_priority??"default",v=Object.entries(l).sort(([,e],[,t])=>(e.name??"").localeCompare(t.name??"")),y=Object.entries(c),w=[...v,...y],x=w.length>0&&w.every(([,e])=>!1!==e.monitoring_enabled),$=w.some(([,e])=>!1!==e.monitoring_enabled),S=v.map(([e,t])=>{const i=De(t.name??e),o=!1!==t.monitoring_enabled,s=!0===t.has_override,a=o?"":"opacity:0.4;",r=De(e);return`\n \n \n \n \n ${i} \n \n \n ${Nt(r,"continuous_threshold_pct",t.continuous_threshold_pct,"%","circuit")}\n ${Nt(r,"spike_threshold_pct",t.spike_threshold_pct,"%","circuit")}\n ${Nt(r,"window_duration_m",t.window_duration_m,"m","circuit")}\n ${Nt(r,"cooldown_duration_m",t.cooldown_duration_m,"m","circuit")}\n \n ${s?`\n ${n("monitoring.reset")}\n `:""}\n \n \n `}).join(""),C=Object.entries(c).map(([e,t])=>{const i=De(t.name??e),o=!1!==t.monitoring_enabled,s=!0===t.has_override,a=o?"":"opacity:0.4;",r=De(e);return`\n \n \n \n \n ${i} \n \n \n ${Nt(r,"continuous_threshold_pct",t.continuous_threshold_pct,"%","mains")}\n ${Nt(r,"spike_threshold_pct",t.spike_threshold_pct,"%","mains")}\n ${Nt(r,"window_duration_m",t.window_duration_m,"m","mains")}\n ${Nt(r,"cooldown_duration_m",t.cooldown_duration_m,"m","mains")}\n \n ${s?`\n ${n("monitoring.reset")}\n `:""}\n \n \n `}).join("");e.innerHTML=`\n \n
${n("monitoring.heading")} \n\n
\n
\n
${n("monitoring.global_settings")} \n \n \n ${n("monitoring.enabled")} \n \n \n\n
\n
\n ${n("monitoring.continuous")} \n \n
\n
\n ${n("monitoring.spike")} \n \n
\n
\n ${n("monitoring.window")} \n \n
\n
\n ${n("monitoring.cooldown")} \n \n
\n\n
\n
${n("notification.heading")} \n\n
\n
${n("notification.targets")} \n
\n \n ${n("notification.all_targets")} \n \n
\n
\n ${g.length?g.map(e=>"event_bus"===e?n("notification.event_bus_target"):De(e)).join(", "):n("notification.none_selected")} \n ▼ \n \n
\n ${0===p.length?`
${n("notification.no_targets")}
`:p.map(e=>{const i=g.includes(e),o="event_bus"===e,s=o?null:t.states[e],a=s?.attributes?.friendly_name,r=o?n("notification.event_bus_target"):a?`${De(a)} (${De(e)})`:De(e);return`
\n \n ${r} \n `}).join("")}\n
\n
\n
\n\n
\n ${n("notification.priority")} \n \n ${["default","passive","active","time-sensitive","critical"].map(e=>`${n(`notification.priority.${e.replace("-","_")}`)} `).join("")}\n \n \n ${"critical"===b?n("notification.hint.critical"):"time-sensitive"===b?n("notification.hint.time_sensitive"):"passive"===b?n("notification.hint.passive"):"active"===b?n("notification.hint.active"):""}\n \n
\n\n
\n ${n("notification.title_template")} \n \n
\n\n
\n ${n("notification.message_template")} \n \n
\n\n
\n ${n("notification.placeholders")} {name} {entity_id} {alert_type}\n {current_a} {breaker_rating_a} {threshold_pct}\n {utilization_pct} {window_m} {local_time}\n
\n
\n ${n("notification.event_bus_help")} span_panel_current_alert\n ${n("notification.event_bus_payload")} alert_source alert_id\n alert_name alert_type current_a\n breaker_rating_a threshold_pct utilization_pct\n panel_serial window_duration_s local_time\n
\n\n
\n ${n("notification.test_label")} \n \n ${n("notification.test_button")}\n \n \n
\n
\n\n
\n
\n\n
${n("monitoring.monitored_points")} \n
\n
\n `;const E=e.querySelector("#toggle-all-circuits");E&&!x&&$&&(E.indeterminate=!0);const k=e.querySelector("#notify-all-targets");if(k&&p.length>0){const e=g.length>0;!_&&e&&(k.indeterminate=!0)}this._bindGlobalControls(e,t),this._bindNotifyTargetSelect(e,t),this._bindNotificationSettings(e,t),this._bindToggleAll(e,t,l,c),this._bindCircuitToggles(e,t),this._bindMainsToggles(e,t),this._bindThresholdInputs(e,t),this._bindResetButtons(e,t)}_serviceData(e){return this._configEntryId&&(e.config_entry_id=this._configEntryId),e}_callSetGlobal(e,t){return e.callWS({type:"call_service",domain:a,service:"set_global_monitoring",service_data:this._serviceData({...t})})}_bindGlobalControls(e,t){const i=e.querySelector("#monitoring-enabled"),o=e.querySelector("#global-fields"),s=e.querySelector("#global-status"),a=()=>{this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{const i={continuous_threshold_pct:parseInt(e.querySelector("#g-continuous").value,10),spike_threshold_pct:parseInt(e.querySelector("#g-spike").value,10),window_duration_m:parseInt(e.querySelector("#g-window").value,10),cooldown_duration_m:parseInt(e.querySelector("#g-cooldown").value,10)};try{await this._callSetGlobal(t,i),await this.render(e,t)}catch(e){if(s){const t=e instanceof Error?e.message:n("error.failed_save");s.textContent=`${n("error.prefix")} ${t}`,s.style.color="var(--error-color, #f44336)"}}},p)};i&&i.addEventListener("change",async()=>{const s=i.checked;o&&(o.style.opacity=s?"":"0.4",o.style.pointerEvents=s?"":"none");const a=e.querySelector("#global-status");try{if(s){const n={continuous_threshold_pct:parseInt(e.querySelector("#g-continuous").value,10),spike_threshold_pct:parseInt(e.querySelector("#g-spike").value,10),window_duration_m:parseInt(e.querySelector("#g-window").value,10),cooldown_duration_m:parseInt(e.querySelector("#g-cooldown").value,10)};await this._callSetGlobal(t,n)}else await this._callSetGlobal(t,{enabled:!1})}catch(e){if(a){const t=e instanceof Error?e.message:n("error.failed");a.textContent=`${n("error.prefix")} ${t}`,a.style.color="var(--error-color, #f44336)"}return}await this.render(e,t)});for(const t of e.querySelectorAll("#global-fields input[type=number]"))t.addEventListener("input",a)}_bindNotifyTargetSelect(e,t){const i=e.querySelector("#notify-target-btn"),o=e.querySelector("#notify-target-dropdown"),s=e.querySelector("#notify-target-label");if(!i||!o)return;i.addEventListener("click",e=>{e.stopPropagation();const t="none"!==o.style.display;o.style.display=t?"none":"block"});const a=t=>{const n=e.querySelector("#notify-target-select");n&&!n.contains(t.target)&&(o.style.display="none")};document.addEventListener("click",a),this._notifyCloseHandler=a;const r=()=>{const i=[...e.querySelectorAll(".notify-target-cb:checked")].map(e=>e.value);if(s){const e=i.map(e=>"event_bus"===e?n("notification.event_bus_target"):e);s.textContent=e.length?e.join(", "):n("notification.none_selected")}const o=e.querySelector("#notify-all-targets");if(o){const t=[...e.querySelectorAll(".notify-target-cb")];o.checked=t.length>0&&t.every(e=>e.checked),o.indeterminate=!o.checked&&t.some(e=>e.checked)}this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{try{await this._callSetGlobal(t,{notify_targets:i.join(", ")})}catch{}},p)},l=e.querySelector("#notify-all-targets");l&&l.addEventListener("change",()=>{for(const t of e.querySelectorAll(".notify-target-cb"))t.checked=l.checked;const t=e.querySelector("#notify-target-btn");t&&(t.style.opacity=l.checked?"0.4":"",t.style.pointerEvents=l.checked?"none":""),l.checked&&(o.style.display="none"),r()});for(const t of e.querySelectorAll(".notify-target-cb"))t.addEventListener("change",()=>{r()})}_bindNotificationSettings(e,t){const i=e.querySelector("#g-priority"),o=e.querySelector("#g-title-template"),s=e.querySelector("#g-message-template"),r=(e,n)=>{this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{try{await this._callSetGlobal(t,{[e]:n})}catch{}},p)};i&&i.addEventListener("change",async()=>{try{await this._callSetGlobal(t,{notification_priority:i.value}),await this.render(e,t)}catch{}}),o&&o.addEventListener("input",()=>{r("notification_title_template",o.value)}),s&&s.addEventListener("input",()=>{r("notification_message_template",s.value)});const l=e.querySelector("#test-notification-btn"),c=e.querySelector("#test-notification-status");l&&l.addEventListener("click",async()=>{l.disabled=!0,c&&(c.textContent=n("notification.test_sending"),c.style.color="var(--secondary-text-color)");try{this._debounceTimer&&(clearTimeout(this._debounceTimer),this._debounceTimer=null);const i=[...e.querySelectorAll(".notify-target-cb:checked")].map(e=>e.value).join(", ");await this._callSetGlobal(t,{notify_targets:i});const o={};this._configEntryId&&(o.config_entry_id=this._configEntryId),await t.callWS({type:"call_service",domain:a,service:"test_notification",service_data:o}),c&&(c.textContent=n("notification.test_sent"),c.style.color="var(--success-color, #4caf50)")}catch(e){if(c){const t=e instanceof Error?e.message:n("error.failed");c.textContent=`${n("error.prefix")} ${t}`,c.style.color="var(--error-color, #f44336)"}}finally{l.disabled=!1}})}_bindToggleAll(e,t,n,i){const o=e.querySelector("#toggle-all-circuits");o&&o.addEventListener("change",async()=>{const s=o.checked,r=[...Object.keys(n).map(e=>t.callWS({type:"call_service",domain:a,service:"set_circuit_threshold",service_data:this._serviceData({circuit_id:e,monitoring_enabled:s})}).catch(()=>{})),...Object.keys(i).map(e=>t.callWS({type:"call_service",domain:a,service:"set_mains_threshold",service_data:this._serviceData({leg:e,monitoring_enabled:s})}).catch(()=>{}))];await Promise.all(r),await this.render(e,t)})}_bindMainsToggles(e,t){for(const n of e.querySelectorAll(".mains-toggle"))n.addEventListener("change",async()=>{const i=n.dataset.entity,o=n.checked;try{await t.callWS({type:"call_service",domain:a,service:"set_mains_threshold",service_data:this._serviceData({leg:i,monitoring_enabled:o})})}catch{return void(n.checked=!o)}await this.render(e,t)})}_bindCircuitToggles(e,t){for(const n of e.querySelectorAll(".circuit-toggle"))n.addEventListener("change",async()=>{const i=n.dataset.entity,o=n.checked;try{await t.callWS({type:"call_service",domain:a,service:"set_circuit_threshold",service_data:this._serviceData({circuit_id:i,monitoring_enabled:o})})}catch{return void(n.checked=!o)}await this.render(e,t)})}_bindThresholdInputs(e,t){const n=new Map;for(const i of e.querySelectorAll(".threshold-input"))i.addEventListener("input",()=>{const o=`${i.dataset.entity}-${i.dataset.field}`,s=n.get(o);s&&clearTimeout(s),n.set(o,setTimeout(async()=>{const n=parseInt(i.value,10);if(!n||n<1)return;const o=i.dataset.entity,s=i.dataset.field,r=i.dataset.type,l="mains"===r?"set_mains_threshold":"set_circuit_threshold",c="mains"===r?"leg":"circuit_id";try{await t.callWS({type:"call_service",domain:a,service:l,service_data:this._serviceData({[c]:o,[s]:n})}),await this.render(e,t)}catch{i.style.borderColor="var(--error-color, #f44336)"}},800))})}_bindResetButtons(e,t){for(const n of e.querySelectorAll(".reset-btn"))n.addEventListener("click",async()=>{const i=n.dataset.entity;if(!i)return;const o=n.dataset.type,s="mains"===o?"clear_mains_threshold":"clear_circuit_threshold",r=this._serviceData("mains"===o?{leg:i}:{circuit_id:i});await t.callService(a,s,r),await this.render(e,t)})}}function Lt(e=""){const t=e?` value="${De(e)}"`:"",i=e?"":"display:none;";return`\n \n \n \n \n \n
\n `}function Ht(e){const t="current"===(e.chart_metric||"power");return`\n \n W \n A \n
\n `}function It(e,t,i,o,s,a,l){const c=t.entities?.power,d=c?i.states[c]:null,h=d&&parseFloat(d.state)||0,p=t.entities?.switch,u=p?i.states[p]:null,g=u?"on"===u.state:(d?.attributes?.relay_state||t.relay_state)===r,f=t.breaker_rating_a,m=f?`${Math.round(f)}A`:"",b=De(t.name||n("grid.unknown")),v=Be(o),y="current"===v.entityRole;let w;if(g)if(y){const e=t.entities?.current,n=e?i.states[e]:null,o=n&&parseFloat(n.state)||0;w=`${v.format(o)} A `}else w=`${je(h)} ${qe(h)} `;else w="";const x=a||"unknown";let $="";if("unknown"!==x){const e=_[x]??_.unknown??{icon:"mdi:help",color:"#999",label:()=>"Unknown"};$=e.icon2?`\n \n \n `:e.textLabel?`\n \n ${e.textLabel} \n `:` `}let S="";if(null!=s?.utilization_pct){const e=s.utilization_pct;S=`${Math.round(e)}% `}const C=g?'ON ':'OFF ';return`\n \n ${m?`${m} `:""}\n ${b} \n ${$}\n ${S}\n ${C}\n \n ${w}\n \n \n \n \n
\n `}function Ot(e,t,n,i,o,s){const a=Ke(e,t,0,"1","single",n,i,o,s,!0);return`${a}
`}function Rt(e){return``}function qt(e,t,n){const i=e.entities?.switch,o=i?t.states[i]:null,s=e.entities?.power,a=s?t.states[s]:null,l=o?"on"===o.state:(a?.attributes?.relay_state||e.relay_state)===r;let c;if("current"===(n.chart_metric||"power")){const n=e.entities?.current,i=n?t.states[n]:null;c=i?Math.abs(parseFloat(i.state)||0):0}else c=a?Math.abs(parseFloat(a.state)||0):0;return{isOn:l,value:c}}function jt(e,t){if(e.always_on)return"always_on";const n=e.entities?.select,i=n?t.states[n]:null;return i?i.state:"unknown"}function Ut(e,t,n){return e.sort((e,i)=>{const o=qt(e[1],t,n),s=qt(i[1],t,n);return o.isOn&&!s.isOn?-1:!o.isOn&&s.isOn?1:s.value-o.value})}function Gt(e){return e.entities?.current??e.entities?.power??""}class Ft{constructor(e){this._expandedUuids=new Set,this._searchQuery="",this._container=null,this._clickHandler=null,this._inputHandler=null,this._graphSettingsHandler=null,this._hass=null,this._topology=null,this._config=null,this._monitoringStatus=null,this._ctrl=e}renderActivityView(e,t,n,i,o){this._unbindEvents(),this._hass=t,this._topology=n,this._config=i,this._monitoringStatus=o;const s=Ut(Object.entries(n.circuits),t,i);let a=Lt(this._searchQuery)+Ht(i);a+='';for(const[e,n]of s){const s=Je(o,Gt(n)),r=jt(n,t),l=this._expandedUuids.has(e);a+=It(e,n,t,i,s,r,l),l&&(a+=Ot(e,n,t,i,s,r))}a+="
",a+=" ",e.innerHTML=a;const r=e.querySelector("span-side-panel");r&&(r.hass=t),this._bindEvents(e),this._searchQuery&&this._applyFilter(e),this._ctrl.updateDOM(e)}renderAreaView(e,t,i,o,s){this._unbindEvents(),this._hass=t,this._topology=i,this._config=o,this._monitoringStatus=s;const a=n("list.unassigned_area"),r=new Map;for(const[e,t]of Object.entries(i.circuits)){const n=t.area??a,i=r.get(n);i?i.push([e,t]):r.set(n,[[e,t]])}const l=[...r.keys()].sort((e,t)=>e===a?1:t===a?-1:e.localeCompare(t));let c=Lt(this._searchQuery)+Ht(o);c+='';for(const e of l){const n=r.get(e);if(!n)continue;const i=Ut(n,t,o);c+=Rt(e);for(const[e,n]of i){const i=Je(s,Gt(n)),a=jt(n,t),r=this._expandedUuids.has(e);c+=It(e,n,t,o,i,a,r),r&&(c+=Ot(e,n,t,o,i,a))}}c+="
",c+=" ",e.innerHTML=c;const d=e.querySelector("span-side-panel");d&&(d.hass=t),this._bindEvents(e),this._searchQuery&&this._applyFilter(e),this._ctrl.updateDOM(e)}updateCollapsedRows(e,t,n,i){const o=Be(i),s="current"===o.entityRole,a=e.querySelectorAll(".list-row[data-row-uuid]");for(const e of a){const a=e.dataset.rowUuid;if(!a)continue;const r=n.circuits[a];if(!r)continue;const{isOn:l,value:c}=qt(r,t,i),d=e.querySelector(".list-power-value");if(d)if(l)if(s)d.innerHTML=`${o.format(c)} A `;else{const e=r.entities?.power,n=e?t.states[e]:null,i=n&&parseFloat(n.state)||0;d.innerHTML=`${je(i)} ${qe(i)} `}else d.innerHTML="";const h=e.querySelector(".list-status-badge");h&&(h.textContent=l?"ON":"OFF",h.classList.toggle("list-status-on",l),h.classList.toggle("list-status-off",!l)),e.classList.toggle("circuit-off",!l)}}stop(){this._unbindEvents(),this._expandedUuids.clear(),this._searchQuery="",this._hass=null,this._topology=null,this._config=null,this._monitoringStatus=null}_bindEvents(e){this._container=e,this._clickHandler=t=>{const n=t.target;if(!n)return;const i=n.closest(".list-expand-toggle");if(i){const e=i.dataset.expandUuid;return void(e&&this._toggleExpand(e))}if(n.closest(".gear-icon"))return void this._ctrl.onGearClick(t,e);if(n.closest(".toggle-pill"))return void this._ctrl.onToggleClick(t,e);if(n.closest(".list-search-clear")){const t=e.querySelector(".list-search");return void(t&&(t.value="",t.dispatchEvent(new Event("input",{bubbles:!0}))))}const o=n.closest(".unit-btn");if(o){const t=o.dataset.unit;t&&e.dispatchEvent(new CustomEvent("unit-changed",{detail:t,bubbles:!0,composed:!0}))}},this._inputHandler=t=>{const n=t.target;n&&n.classList.contains("list-search")&&(this._searchQuery=n.value.toLowerCase(),this._applyFilter(e))},this._graphSettingsHandler=()=>{this._ctrl.onGraphSettingsChanged(e).then(()=>{this._ctrl.updateDOM(e)}).catch(()=>{})},e.addEventListener("click",this._clickHandler),e.addEventListener("input",this._inputHandler),e.addEventListener("graph-settings-changed",this._graphSettingsHandler)}_unbindEvents(){this._container&&(this._clickHandler&&this._container.removeEventListener("click",this._clickHandler),this._inputHandler&&this._container.removeEventListener("input",this._inputHandler),this._graphSettingsHandler&&this._container.removeEventListener("graph-settings-changed",this._graphSettingsHandler)),this._container=null,this._clickHandler=null,this._inputHandler=null,this._graphSettingsHandler=null}_applyFilter(e){const t=e.querySelector(".list-search-clear");t&&(t.style.display=this._searchQuery?"":"none");const n=e.querySelectorAll(".list-row[data-row-uuid]");for(const t of n){const n=t.querySelector(".list-circuit-name"),i=(n?.textContent?.toLowerCase()??"").includes(this._searchQuery);t.style.display=i?"":"none";const o=t.dataset.rowUuid;if(o){const t=e.querySelector(`.list-expanded-content[data-expanded-uuid="${o}"]`);t&&(t.style.display=i?"":"none")}}const i=e.querySelectorAll(".area-header");for(const e of i){let t=!1,n=e.nextElementSibling;for(;n&&!n.classList.contains("area-header");){if(n.classList.contains("list-row")&&"none"!==n.style.display){t=!0;break}n=n.nextElementSibling}e.style.display=t?"":"none"}}_toggleExpand(e){if(!(this._container&&this._hass&&this._topology&&this._config))return;const t=this._container.querySelector(`.list-row[data-row-uuid="${e}"]`),n=this._container.querySelector(`.list-expand-toggle[data-expand-uuid="${e}"]`);if(t)if(this._expandedUuids.has(e)){this._expandedUuids.delete(e);const i=this._container.querySelector(`.list-expanded-content[data-expanded-uuid="${e}"]`);i&&i.remove(),n&&n.classList.remove("expanded"),t.classList.remove("list-row-expanded")}else{this._expandedUuids.add(e);const i=this._topology.circuits[e];if(!i)return;const o=Je(this._monitoringStatus,Gt(i)),s=jt(i,this._hass),a=Ot(e,i,this._hass,this._config,o,s);t.insertAdjacentHTML("afterend",a),n&&n.classList.add("expanded"),t.classList.add("list-row-expanded"),this._ctrl.updateDOM(this._container)}}}let Wt=class extends $e{constructor(){super(...arguments),this.narrow=!1,this._panels=[],this._selectedPanelId=null,this._activeTab="dashboard",this._discovered=!1,this._discoveryError=null,this._dashboardTab=new kt,this._monitoringTab=new Dt,this._listDashCtrl=new Ct,this._listCtrl=new Ft(this._listDashCtrl),this._areaUnsub=null,this._onVisibilityChange=null,this._deviceRegistryUnsub=null}connectedCallback(){super.connectedCallback(),this._onVisibilityChange=()=>{"visible"===document.visibilityState&&this._discovered&&this.hass&&this._scheduleTabRender()},document.addEventListener("visibilitychange",this._onVisibilityChange),this._subscribeDeviceRegistry()}disconnectedCallback(){this._dashboardTab.stop(),this._monitoringTab.stop(),this._listCtrl.stop(),this._listDashCtrl.stopIntervals(),this._areaUnsub&&(this._areaUnsub(),this._areaUnsub=null),this._onVisibilityChange&&(document.removeEventListener("visibilitychange",this._onVisibilityChange),this._onVisibilityChange=null),this._unsubscribeDeviceRegistry(),super.disconnectedCallback()}firstUpdated(){this.hass&&!this._discovered&&this._discoverPanels()}updated(e){if(e.has("hass")){const t=e.get("hass");this._dashboardTab.hass=this.hass,this._listDashCtrl.hass=this.hass;const n=this.renderRoot.querySelector("ha-menu-button");n&&(n.hass=this.hass,n.narrow=this.narrow),this._discovered?this.shadowRoot.getElementById("tab-content")||this._scheduleTabRender():this._discoverPanels(),!t&&this.hass&&this._subscribeDeviceRegistry()}if(e.has("narrow")){const e=this.renderRoot.querySelector("ha-menu-button");e&&(e.narrow=this.narrow)}if(this._discovered&&(e.has("_discovered")||e.has("_activeTab")||e.has("_selectedPanelId")||e.has("_chartMetric"))&&this._scheduleTabRender(),e.has("hass")&&this._discovered&&("activity"===this._activeTab||"area"===this._activeTab)){const e=this.shadowRoot.getElementById("tab-content"),t=this._listDashCtrl.topology;if(e&&t){this._listCtrl.updateCollapsedRows(e,this.hass,t,this._buildDashboardConfig());const n=e.querySelector("span-side-panel");n&&(n.hass=this.hass)}}}setConfig(e){}render(){var i,o,s;return i=this.hass?.language,e=i&&t[i]?i:"en",this._discovered?se`
+ */class De extends Te{constructor(e){if(super(e),this.it=de,e.type!==Ie)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(e){if(e===de||null==e)return this._t=void 0,this.it=e;if(e===ce)return e;if("string"!=typeof e)throw Error(this.constructor.directiveName+"() called with a non-string value");if(e===this.it)return this._t;this.it=e;const t=[e];return t.raw=t,this._t={_$litType$:this.constructor.resultType,strings:t,values:[]}}}De.directiveName="unsafeHTML",De.resultType=1;const Fe=(e=>(...t)=>({_$litDirective$:e,values:t}))(De);class Le{constructor(){this._persistent=new Map,this._transient=null,this._transientTimer=null,this._subscribers=new Set,this._watchedPanels=new Map}add(e){const t={...e,timestamp:Date.now()};if(t.persistent)this._persistent.set(t.key,t);else{this._clearTransient(),this._transient=t;const e=t.ttl??5e3;this._transientTimer=setTimeout(()=>{this._transient=null,this._transientTimer=null,this._notify()},e)}this._notify()}remove(e){if(this._persistent.has(e))return this._persistent.delete(e),void this._notify();this._transient?.key===e&&(this._clearTransient(),this._notify())}clear(e){void 0===e?(this._persistent.clear(),this._clearTransient(),this._watchedPanels.clear()):!0===e.persistent?this._persistent.clear():!1===e.persistent&&this._clearTransient(),this._notify()}get active(){const e=[...this._persistent.values()];return null!==this._transient&&e.push(this._transient),e}hasPersistent(e){return this._persistent.has(e)}hasAnyPanelOffline(){for(const e of this._persistent.keys())if("panel-offline"===e||e.startsWith("panel-offline:"))return!0;return!1}subscribe(e){return this._subscribers.add(e),()=>{this._subscribers.delete(e)}}watchPanelStatus(e){this.watchPanelStatuses([{entityId:e,panelName:null}])}watchPanelStatuses(e){const t=this._watchedPanels,n=new Map;for(const i of e){const e=t.get(i.entityId);n.set(i.entityId,{panelName:i.panelName??null,wasOffline:e?.wasOffline??!1})}const i=this._isSingleUnnamed(t),s=this._isSingleUnnamed(n);for(const e of t.keys()){n.has(e)&&i===s||this._persistent.delete(this._offlineKey(e,i))}this._watchedPanels=n,this._notify()}clearPanelStatusWatch(){if(0===this._watchedPanels.size)return;const e=this._isSingleUnnamed(this._watchedPanels);for(const t of this._watchedPanels.keys())this._persistent.delete(this._offlineKey(t,e));this._watchedPanels.clear(),this._notify()}updateHass(e){if(0===this._watchedPanels.size)return;const t=this._isSingleUnnamed(this._watchedPanels);for(const[s,o]of this._watchedPanels){const r=e.states[s]?.state,a="on"===r,l=this._offlineKey(s,t),c=this._reconnectKey(s,t);if(a){const e=o.wasOffline;o.wasOffline=!1,this.remove(l),e&&this.add({key:c,level:"info",message:null===o.panelName?n("error.panel_reconnected"):i("error.panel_reconnected_named",{name:o.panelName}),persistent:!1})}else o.wasOffline=!0,this.hasPersistent(l)||this.add({key:l,level:"error",message:null===o.panelName?n("error.panel_offline"):i("error.panel_offline_named",{name:o.panelName}),persistent:!0})}}dispose(){this._clearTransient(),this._persistent.clear(),this._subscribers.clear(),this._watchedPanels.clear()}_isSingleUnnamed(e){if(1!==e.size)return!1;for(const t of e.values())return null===t.panelName;return!1}_offlineKey(e,t){return t?"panel-offline":`panel-offline:${e}`}_reconnectKey(e,t){return t?"panel-reconnected":`panel-reconnected:${e}`}_clearTransient(){null!==this._transientTimer&&(clearTimeout(this._transientTimer),this._transientTimer=null),this._transient=null}_notify(){for(const e of this._subscribers)try{e()}catch(e){console.warn("SPAN Panel: error-store subscriber threw",e)}}}const He={"&":"&","<":"<",">":">",'"':""","'":"'"};function Oe(e){return String(e).replace(/[&<>"']/g,e=>He[e]??e)}const Re="span_panel_list_columns";function qe(){try{const e=localStorage.getItem(Re);if(!e)return 1;const t=parseInt(e,10);return 1===t||2===t||3===t?t:1}catch{return 1}}function je(e){try{localStorage.setItem(Re,String(e))}catch{}}function Ue(e){return new Promise(t=>setTimeout(t,e))}class We{constructor(e){this._store=e}async callWS(e,t,n){const i=n?.retries??3,s=n?.errorId??`ws:${String(t.type??"unknown")}`;return this._withRetry(()=>e.callWS(t),i,s,n?.errorMessage)}async callService(e,t,n,i,s,o){const r=o?.retries??3,a=o?.errorId??`svc:${t}.${n}`;return this._withRetry(()=>e.callService(t,n,i,s),r,a,o?.errorMessage)}async _withRetry(e,t,i,s){if(this._store.hasAnyPanelOffline())try{const t=await e();return this._store.remove(i),t}catch(e){const t=e instanceof Error?e:new Error(String(e));throw this._store.add({key:i,level:"error",message:s??n("error.panel_offline"),persistent:!1}),t}let o;for(let n=0;n<=t;n++)try{const t=await e();return this._store.remove(i),t}catch(e){if(o=e instanceof Error?e:new Error(String(e)),n{try{const t={type:"call_service",domain:a,service:"get_favorites",service_data:{},return_response:!0},s=this._retry?await this._retry.callWS(e,t,{errorId:"fetch:favorites",errorMessage:n("error.favorites_fetch_failed")}):await e.callWS(t),o=s?.response?.favorites??{};return i===this._generation&&(this._map=o,this._lastFetch=Date.now()),o}catch(e){return console.warn("SPAN Panel: favorites fetch failed",e),this._retry||this._errorStore?.add({key:"fetch:favorites",level:"warning",message:n("error.favorites_fetch_failed"),persistent:!1}),this._map??{}}finally{this._inflight?.gen===i&&(this._inflight=null)}})();return this._inflight={gen:i,promise:s},s}invalidate(){this._lastFetch=0,this._generation++}clear(){this._map=null,this._lastFetch=0,this._generation++}get map(){return this._map??{}}}function Qe(e){for(const t of Object.values(e)){if((t.circuits?.length??0)>0)return!0;if((t.sub_devices?.length??0)>0)return!0}return!1}const Ke=Object.keys(f).filter(e=>"unknown"!==e&&"always_on"!==e);class Je extends HTMLElement{constructor(){super(),this.errorStore=null,this.attachShadow({mode:"open"}),this._hass=null,this._config=null,this._debounceTimers={}}set hass(e){this._hass=e,this.hasAttribute("open")&&this._config&&this._updateLiveState()}get hass(){return this._hass}disconnectedCallback(){this._clearDebounceTimers(),this._config=null}open(e){this._config=e,this._render(),this.offsetHeight,this.setAttribute("open",""),this.setAttribute("data-mode",this._modeFor(e))}close(){this._clearDebounceTimers(),this.removeAttribute("open"),this.removeAttribute("data-mode"),this._config=null,this.dispatchEvent(new CustomEvent("side-panel-closed",{bubbles:!0,composed:!0}))}_clearDebounceTimers(){for(const e of Object.keys(this._debounceTimers))clearTimeout(this._debounceTimers[e]);this._debounceTimers={}}_modeFor(e){return e.favoritesMode?"favorites":e.panelMode?"panel":e.subDeviceMode?"subDevice":"circuit"}_render(){const e=this._config;if(!e)return;const t=this.shadowRoot;if(!t)return;t.innerHTML="";const n=document.createElement("style");n.textContent='\n :host {\n display: block;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n width: 360px;\n max-width: 90vw;\n z-index: 1000;\n transform: translateX(100%);\n transition: transform 0.3s ease;\n pointer-events: none;\n }\n :host([open]) {\n transform: translateX(0);\n pointer-events: auto;\n }\n\n .backdrop {\n display: none;\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: rgba(0, 0, 0, 0.3);\n z-index: -1;\n }\n :host([open]) .backdrop {\n display: block;\n }\n\n .panel {\n height: 100%;\n background: var(--card-background-color, #fff);\n border-left: 1px solid var(--divider-color, #e0e0e0);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n }\n\n .panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 16px;\n border-bottom: 1px solid var(--divider-color, #e0e0e0);\n }\n .panel-header .title {\n font-size: 18px;\n font-weight: 500;\n color: var(--primary-text-color, #212121);\n margin: 0;\n }\n .panel-header .subtitle {\n font-size: 13px;\n color: var(--secondary-text-color, #727272);\n margin: 2px 0 0 0;\n }\n .close-btn {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color, #727272);\n padding: 4px;\n line-height: 1;\n font-size: 20px;\n }\n\n .panel-body {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n }\n\n .section {\n margin-bottom: 20px;\n }\n .section-label {\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n color: var(--secondary-text-color, #727272);\n margin: 0 0 8px 0;\n letter-spacing: 0.5px;\n }\n\n .field-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 0;\n }\n .field-label {\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n }\n\n select {\n padding: 6px 8px;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 4px;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n font-size: 14px;\n }\n\n input[type="number"] {\n width: 72px;\n padding: 6px 8px;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 4px;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n font-size: 14px;\n text-align: right;\n }\n input[type="number"]:disabled {\n opacity: 0.5;\n }\n\n .radio-group {\n display: flex;\n gap: 16px;\n padding: 8px 0;\n }\n .radio-group label {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n cursor: pointer;\n }\n\n .horizon-bar {\n display: flex;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 6px;\n overflow: hidden;\n margin-top: 4px;\n }\n .horizon-segment {\n flex: 1;\n padding: 6px 0;\n text-align: center;\n font-size: 13px;\n cursor: pointer;\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n border: none;\n border-right: 1px solid var(--divider-color, #e0e0e0);\n transition: background 0.15s ease, color 0.15s ease;\n user-select: none;\n line-height: 1.4;\n }\n .horizon-segment:last-child {\n border-right: none;\n }\n .horizon-segment:hover:not(.active) {\n background: var(--secondary-background-color, #f5f5f5);\n }\n .horizon-segment.active {\n background: var(--primary-color, #03a9f4);\n color: #fff;\n font-weight: 600;\n }\n .horizon-segment.referenced {\n box-shadow: inset 0 -3px 0 var(--primary-color, #03a9f4);\n }\n\n .unit-toggle {\n display: inline-flex;\n border: 1px solid var(--divider-color, #e0e0e0);\n border-radius: 6px;\n overflow: hidden;\n }\n .unit-btn {\n padding: 4px 10px;\n border: none;\n border-right: 1px solid var(--divider-color, #e0e0e0);\n background: var(--card-background-color, #fff);\n color: var(--primary-text-color, #212121);\n font-size: 13px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.15s ease, color 0.15s ease;\n }\n .unit-btn:last-child {\n border-right: none;\n }\n .unit-btn:hover:not(.unit-active) {\n background: var(--secondary-background-color, #f5f5f5);\n }\n .unit-btn.unit-active {\n background: var(--primary-color, #03a9f4);\n color: #fff;\n font-weight: 600;\n }\n\n .monitoring-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n }\n\n .fav-heart {\n background: none;\n border: 1px solid var(--divider-color, #e0e0e0);\n color: var(--secondary-text-color, #727272);\n border-radius: 4px;\n padding: 2px 6px;\n cursor: pointer;\n font-size: 0.9em;\n margin-right: 6px;\n line-height: 1;\n display: inline-flex;\n align-items: center;\n }\n .fav-heart.active {\n color: var(--primary-color, #03a9f4);\n border-color: var(--primary-color, #03a9f4);\n }\n .fav-heart:hover:not(.active) {\n background: var(--secondary-background-color, #f5f5f5);\n }\n .fav-heart ha-icon {\n --mdc-icon-size: 16px;\n }\n\n .panel-mode-info {\n font-size: 14px;\n color: var(--primary-text-color, #212121);\n line-height: 1.6;\n }\n .panel-mode-info p {\n margin: 0 0 12px 0;\n }\n\n',t.appendChild(n);const i=document.createElement("div");i.className="backdrop",i.addEventListener("click",()=>this.close()),t.appendChild(i);const s=document.createElement("div");s.className="panel",t.appendChild(s),e.favoritesMode?this._renderFavoritesMode(s):e.panelMode?this._renderPanelMode(s):e.subDeviceMode?this._renderSubDeviceMode(s,e):this._renderCircuitMode(s,e)}_renderPanelMode(e){const t=this._config,i=this._createHeader(n("sidepanel.graph_settings"),n("sidepanel.global_defaults"));e.appendChild(i);const s=document.createElement("div");s.className="panel-body";const a=t.graphSettings,l=t.topology,c=a?.global_horizon??o,d=a?.circuits??{};s.appendChild(this._buildListColumnsSection());const h=document.createElement("div");h.className="section";const p=document.createElement("div");p.className="section-label",p.textContent=n("sidepanel.graph_horizon"),h.appendChild(p);const g=document.createElement("div");g.className="field-row";const _=document.createElement("span");_.className="field-label",_.textContent=n("sidepanel.global_default"),g.appendChild(_);const f=document.createElement("select");for(const e of Object.keys(r)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,s=n(i);t.textContent=s!==i?s:e,e===c&&(t.selected=!0),f.appendChild(t)}if(f.addEventListener("change",()=>{const e={horizon:f.value};t.configEntryId&&(e.config_entry_id=t.configEntryId),this._callDomainService("set_graph_time_horizon",e).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})}),g.appendChild(f),h.appendChild(g),s.appendChild(h),l?.circuits){const e=document.createElement("div");e.className="section";const i=document.createElement("div");i.className="section-label",i.textContent=n("sidepanel.circuit_scales"),e.appendChild(i);const o=Object.entries(l.circuits).sort(([,e],[,t])=>(e.name||"").localeCompare(t.name||""));for(const[n,i]of o){const s=this._buildPanelModeCircuitRow(n,i,d[n],c,t.configEntryId??null,t.showFavorites??!1,t.favoritePanelDeviceId,t.favoriteCircuitUuids);e.appendChild(s)}s.appendChild(e)}const v=a?.sub_devices??{};if(l?.sub_devices){const e=document.createElement("div");e.className="section";const i=document.createElement("div");i.className="section-label",i.textContent=n("sidepanel.subdevice_scales"),e.appendChild(i);const o=Object.entries(l.sub_devices).sort(([,e],[,t])=>(e.name||"").localeCompare(t.name||""));for(const[i,s]of o){const o=document.createElement("div");o.className="field-row";const a=document.createElement("span");if(a.className="field-label",a.textContent=s.name||i,a.style.cssText="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;",o.appendChild(a),t.showFavorites&&t.favoritePanelDeviceId){const e=this._buildSubDeviceFavoriteHeart(s.entities,t.favoriteSubDeviceIds?.has(i)??!1);e&&o.appendChild(e)}const l=v[i]||{horizon:c,has_override:!1},d=l.has_override?l.horizon:c,h=document.createElement("select");h.dataset.subdevId=i;for(const e of Object.keys(r)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,s=n(i);t.textContent=s!==i?s:e,e===d&&(t.selected=!0),h.appendChild(t)}if(h.addEventListener("change",()=>{this._debounce(`subdev-${i}`,u,()=>{const e={subdevice_id:i,horizon:h.value};t.configEntryId&&(e.config_entry_id=t.configEntryId),this._callDomainService("set_subdevice_graph_horizon",e).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})})}),o.appendChild(h),l.has_override){const e=document.createElement("button");e.textContent="↺",e.title=n("sidepanel.reset_to_global"),Object.assign(e.style,{background:"none",border:"1px solid var(--divider-color, #e0e0e0)",color:"var(--primary-text-color)",borderRadius:"4px",padding:"3px 6px",cursor:"pointer",marginLeft:"4px",fontSize:"0.85em"}),e.addEventListener("click",()=>{const s={subdevice_id:i};t.configEntryId&&(s.config_entry_id=t.configEntryId),this._callDomainService("clear_subdevice_graph_horizon",s).then(()=>{h.value=c,e.remove(),this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})}),o.appendChild(e)}e.appendChild(o)}s.appendChild(e)}e.appendChild(s)}_buildPanelModeCircuitRow(e,t,i,s,o,a,l,c){const d=document.createElement("div");d.className="field-row";const h=document.createElement("span");if(h.className="field-label",h.textContent=t.name||e,h.style.cssText="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;flex:1;",d.appendChild(h),a&&l){const n=this._buildFavoriteHeart(t.entities,c?.has(e)??!1);n&&d.appendChild(n)}const p=i||{horizon:s,has_override:!1},g=p.has_override?p.horizon:s,_=document.createElement("select");_.dataset.uuid=e;for(const e of Object.keys(r)){const t=document.createElement("option");t.value=e;const i=`horizon.${e}`,s=n(i);t.textContent=s!==i?s:e,e===g&&(t.selected=!0),_.appendChild(t)}if(_.addEventListener("change",()=>{this._debounce(`circuit-${e}`,u,()=>{const t={circuit_id:e,horizon:_.value};o&&(t.config_entry_id=o),this._callDomainService("set_circuit_graph_horizon",t).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})})}),d.appendChild(_),p.has_override){const t=document.createElement("button");t.textContent="↺",t.title=n("sidepanel.reset_to_global"),Object.assign(t.style,{background:"none",border:"1px solid var(--divider-color, #e0e0e0)",color:"var(--primary-text-color)",borderRadius:"4px",padding:"3px 6px",cursor:"pointer",marginLeft:"4px",fontSize:"0.85em"}),t.addEventListener("click",()=>{const i={circuit_id:e};o&&(i.config_entry_id=o),this._callDomainService("clear_circuit_graph_horizon",i).then(()=>{_.value=s,t.remove(),this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})}),d.appendChild(t)}return d}_renderFavoritesMode(e){const t=this._config,i=this._createHeader(n("sidepanel.graph_settings"),n("sidepanel.favorites_subtitle"));e.appendChild(i);const s=document.createElement("div");s.className="panel-body",s.appendChild(this._buildListColumnsSection());for(const e of t.perPanelSections)s.appendChild(this._buildFavoritesPanelSection(e));e.appendChild(s)}_buildFavoritesPanelSection(e){const t=document.createElement("div");t.className="section";const n=document.createElement("div");n.className="section-label",n.textContent=e.panelName,t.appendChild(n);const i=e.graphSettings?.global_horizon??o,s=e.graphSettings?.circuits??{},r=function(e){const t=e.circuits??{};return Object.entries(t).map(([e,t])=>({uuid:e,circuit:t})).sort((e,t)=>(e.circuit.name||"").localeCompare(t.circuit.name||""))}(e.topology);for(const{uuid:n,circuit:o}of r){const r=this._buildPanelModeCircuitRow(n,o,s[n],i,e.configEntryId,!0,e.panelDeviceId,e.favoriteCircuitUuids);t.appendChild(r)}return t}_renderCircuitMode(e,t){const n=`${Oe(String(t.breaker_rating_a))}A · ${Oe(String(t.voltage))}V · Tabs [${Oe(String(t.tabs))}]`,i=this._createHeader(Oe(t.name),n);e.appendChild(i);const s=document.createElement("div");s.className="panel-body",e.appendChild(s),this._renderRelaySection(s,t),t.showFavorites&&this._renderFavoriteSection(s,t),this._renderSheddingSection(s,t),this._renderGraphHorizonSection(s,t),t.showMonitoring&&this._renderMonitoringSection(s,t)}_favoriteEntityId(e){return e?.current??e?.power??null}_subDeviceFavoriteEntityId(e){if(!e)return null;let t=null;for(const[n,i]of Object.entries(e)){if("sensor"===i.domain)return n;t||(t=n)}return t}_buildSubDeviceFavoriteHeart(e,t){const n=this._subDeviceFavoriteEntityId(e);return n?this._buildHeartButton(n,t):null}_buildListColumnsSection(){const e=document.createElement("div");e.className="section";const t=document.createElement("div");t.className="section-label",t.textContent=n("sidepanel.list_view_columns"),e.appendChild(t);const i=document.createElement("div");i.className="field-row";const s=document.createElement("span");s.className="field-label",s.textContent=n("sidepanel.columns"),i.appendChild(s);const o=qe(),r=document.createElement("div");r.className="unit-toggle";for(const e of[1,2,3]){const t=document.createElement("button");t.type="button",t.className="unit-btn"+(e===o?" unit-active":""),t.dataset.columns=String(e),t.textContent=String(e),t.addEventListener("click",()=>{je(e);for(const e of r.querySelectorAll(".unit-btn"))e.classList.toggle("unit-active",e===t);this.dispatchEvent(new CustomEvent("list-columns-changed",{detail:e,bubbles:!0,composed:!0}))}),r.appendChild(t)}return i.appendChild(r),e.appendChild(i),e}_buildFavoriteHeart(e,t){const n=this._favoriteEntityId(e);return n?this._buildHeartButton(n,t):(console.warn("SPAN Panel: circuit has no current/power sensor; favorite heart suppressed"),null)}_buildHeartButton(e,t){const i=document.createElement("button");i.type="button",i.className=t?"fav-heart active":"fav-heart",i.dataset.role="fav-heart",i.title=n("sidepanel.save_to_favorites"),i.setAttribute("role","switch"),i.setAttribute("aria-checked",String(t)),i.setAttribute("aria-label",n("sidepanel.save_to_favorites"));const s=document.createElement("ha-icon");return s.setAttribute("icon",t?"mdi:heart":"mdi:heart-outline"),i.appendChild(s),i.addEventListener("click",t=>{t.stopPropagation(),this._toggleFavoriteEntity(i,s,e).catch(()=>{})}),i}async _toggleFavoriteEntity(e,t,i){if(!this._hass)return;const s=e.classList.contains("active"),o=!s;e.classList.toggle("active",o),t.setAttribute("icon",o?"mdi:heart":"mdi:heart-outline"),e.setAttribute("aria-checked",String(o));try{o?await async function(e,t){const n=await Ve(e,"add_favorite",{entity_id:t});return document.dispatchEvent(new CustomEvent(Ge)),n?.favorites??{}}(this._hass,i):await async function(e,t){const n=await Ve(e,"remove_favorite",{entity_id:t});return document.dispatchEvent(new CustomEvent(Ge)),n?.favorites??{}}(this._hass,i)}catch(i){throw e.classList.toggle("active",s),t.setAttribute("icon",s?"mdi:heart":"mdi:heart-outline"),e.setAttribute("aria-checked",String(s)),console.warn("SPAN Panel: favorite toggle failed",i),this.errorStore?.add({key:"service:favorites",level:"error",message:n("error.favorites_toggle_failed"),persistent:!1}),i}}_renderFavoriteSection(e,t){const n=this._favoriteEntityId(t.entities);n&&this._appendFavoriteHeartSection(e,n,!0===t.isFavorite)}_appendFavoriteHeartSection(e,t,i){const s=document.createElement("div");s.className="section",s.innerHTML=`${Oe(n("sidepanel.favorite"))}
`;const o=document.createElement("div");o.className="field-row";const r=document.createElement("span");r.className="field-label",r.textContent=n("sidepanel.save_to_favorites"),o.appendChild(r),o.appendChild(this._buildHeartButton(t,i)),s.appendChild(o),e.appendChild(s)}_renderSubDeviceMode(e,t){const n=this._createHeader(Oe(t.name),Oe(t.deviceType));e.appendChild(n);const i=document.createElement("div");i.className="panel-body",e.appendChild(i),t.showFavorites&&this._renderSubDeviceFavoriteSection(i,t),this._renderSubDeviceHorizonSection(i,t)}_renderSubDeviceFavoriteSection(e,t){const n=this._subDeviceFavoriteEntityId(t.entities);n&&this._appendFavoriteHeartSection(e,n,!0===t.isFavorite)}_renderSubDeviceHorizonSection(e,t){const i=document.createElement("div");i.className="section";const s=document.createElement("div");s.className="section-label",s.textContent=n("sidepanel.graph_horizon"),i.appendChild(s);const a=t.graphHorizonInfo,l=!0===a?.has_override,c=a?.horizon||o,d=a?.globalHorizon||o,h=document.createElement("div");h.className="horizon-bar";const p=[{key:"global",label:n("sidepanel.global")}];for(const e of Object.keys(r))p.push({key:e,label:e});const u=l?c:"global",g=e=>{for(const t of h.querySelectorAll(".horizon-segment")){const n=t.dataset.horizon;t.classList.toggle("active",n===e),t.classList.toggle("referenced","global"===e&&n===d)}};for(const{key:e,label:i}of p){const s=document.createElement("button");s.type="button",s.className="horizon-segment",s.dataset.horizon=e,s.textContent=i,s.classList.toggle("active",e===u),s.classList.toggle("referenced","global"===u&&e===d),s.addEventListener("click",()=>{if(s.classList.contains("active"))return;const i={subdevice_id:t.subDeviceId};t.configEntryId&&(i.config_entry_id=t.configEntryId),"global"===e?(g("global"),this._callDomainService("clear_subdevice_graph_horizon",i).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})):(g(e),this._callDomainService("set_subdevice_graph_horizon",{...i,horizon:e}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})}))}),h.appendChild(s)}i.appendChild(h),e.appendChild(i)}_createHeader(e,t){const n=document.createElement("div");n.className="panel-header";const i=document.createElement("div"),s=Oe(e),o=Oe(t);i.innerHTML=`${s}
`+(o?`${o}
`:"");const r=document.createElement("button");return r.className="close-btn",r.innerHTML="✕",r.addEventListener("click",()=>this.close()),n.appendChild(i),n.appendChild(r),n}_renderRelaySection(e,t){if(!1===t.is_user_controllable||!t.entities?.switch)return;const i=document.createElement("div");i.className="section",i.innerHTML=`${Oe(n("sidepanel.relay"))}
`;const s=document.createElement("div");s.className="field-row";const o=document.createElement("span");o.className="field-label",o.textContent=n("sidepanel.breaker");const r=document.createElement("ha-switch");r.dataset.role="relay-toggle";const a=t.entities.switch,l=this._hass?.states?.[a]?.state;"on"===l&&r.setAttribute("checked",""),r.addEventListener("change",()=>{const e=r.hasAttribute("checked")||r.checked;this._callService("switch",e?"turn_on":"turn_off",{entity_id:a}).catch(e=>{console.warn("SPAN Panel: relay toggle failed",e),this.errorStore?.add({key:"service:relay",level:"error",message:n("error.relay_failed"),persistent:!1})})}),s.appendChild(o),s.appendChild(r),i.appendChild(s),e.appendChild(i)}_renderSheddingSection(e,t){if(!t.entities?.select)return;const i=document.createElement("div");i.className="section",i.innerHTML=`${Oe(n("sidepanel.shedding_priority"))}
`;const s=document.createElement("div");s.className="field-row";const o=document.createElement("span");o.className="field-label",o.textContent=n("sidepanel.priority_label");const r=document.createElement("select");r.dataset.role="shedding-select";const a=t.entities.select,l=this._hass?.states?.[a]?.state||"";for(const e of Ke){const t=f[e];if(!t)continue;const i=document.createElement("option");i.value=e,i.textContent=n(`shedding.select.${e}`)||t.label(),e===l&&(i.selected=!0),r.appendChild(i)}r.addEventListener("change",()=>{this._callService("select","select_option",{entity_id:a,option:r.value}).catch(e=>{console.warn("SPAN Panel: shedding update failed",e),this.errorStore?.add({key:"service:shedding",level:"error",message:n("error.shedding_failed"),persistent:!1})})}),s.appendChild(o),s.appendChild(r),i.appendChild(s),e.appendChild(i)}_renderGraphHorizonSection(e,t){const i=document.createElement("div");i.className="section";const s=document.createElement("div");s.className="section-label",s.textContent=n("sidepanel.graph_horizon"),i.appendChild(s);const a=t.graphHorizonInfo,l=!0===a?.has_override,c=a?.horizon||o,d=a?.globalHorizon||o,h=document.createElement("div");h.className="horizon-bar";const p=[{key:"global",label:n("sidepanel.global")}];for(const e of Object.keys(r))p.push({key:e,label:e});const u=l?c:"global",g=e=>{for(const t of h.querySelectorAll(".horizon-segment")){const n=t.dataset.horizon;t.classList.toggle("active",n===e),t.classList.toggle("referenced","global"===e&&n===d)}};for(const{key:e,label:i}of p){const s=document.createElement("button");s.type="button",s.className="horizon-segment",s.dataset.horizon=e,s.textContent=i,s.classList.toggle("active",e===u),s.classList.toggle("referenced","global"===u&&e===d),s.addEventListener("click",()=>{if(s.classList.contains("active"))return;const i={circuit_id:t.uuid};t.configEntryId&&(i.config_entry_id=t.configEntryId),"global"===e?(g("global"),this._callDomainService("clear_circuit_graph_horizon",i).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})})):(g(e),this._callDomainService("set_circuit_graph_horizon",{...i,horizon:e}).then(()=>{this.dispatchEvent(new CustomEvent("graph-settings-changed",{bubbles:!0,composed:!0}))}).catch(e=>{console.warn("SPAN Panel: graph horizon service failed",e),this.errorStore?.add({key:"service:graph_horizon",level:"error",message:n("error.graph_horizon_failed"),persistent:!1})}))}),h.appendChild(s)}i.appendChild(h),e.appendChild(i)}_renderMonitoringSection(e,t){const i=document.createElement("div");i.className="section";const s=document.createElement("div");s.className="monitoring-header";const o=document.createElement("div");o.className="section-label",o.textContent=n("sidepanel.monitoring"),o.style.margin="0";const r=document.createElement("ha-switch");r.dataset.role="monitoring-toggle";const a=t.monitoringInfo,l=null!=a&&!1!==a.monitoring_enabled;l&&r.setAttribute("checked",""),s.appendChild(o),s.appendChild(r),i.appendChild(s);const c=document.createElement("div");c.dataset.role="monitoring-details",c.style.display=l?"block":"none",i.appendChild(c);const d=!0===a?.has_override,h=document.createElement("div");h.className="radio-group",h.innerHTML=`\n ${Oe(n("sidepanel.global"))} \n ${Oe(n("sidepanel.custom"))} \n `,c.appendChild(h);const p=document.createElement("div");p.dataset.role="threshold-fields",p.style.display=d?"block":"none";const u=a?.continuous_threshold_pct??80,g=a?.spike_threshold_pct??100,_=a?.window_duration_m??15,f=a?.cooldown_duration_m??15;p.appendChild(this._createThresholdRow(n("sidepanel.continuous_pct"),"continuous",u,t)),p.appendChild(this._createThresholdRow(n("sidepanel.spike_pct"),"spike",g,t)),p.appendChild(this._createDurationRow(n("sidepanel.window_duration"),"window-m",_,1,180,"m",t)),p.appendChild(this._createDurationRow(n("sidepanel.cooldown"),"cooldown-m",f,1,180,"m",t)),c.appendChild(p),r.addEventListener("change",()=>{const e=r.checked;c.style.display=e?"block":"none";const i={circuit_id:t.entities?.power||t.uuid,monitoring_enabled:e};t.configEntryId&&(i.config_entry_id=t.configEntryId),this._callDomainService("set_circuit_threshold",i).catch(e=>{console.warn("SPAN Panel: monitoring update failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})})});const v=h.querySelectorAll('input[type="radio"]');for(const e of v)e.addEventListener("change",()=>{const i="custom"===e.value&&e.checked;if(p.style.display=i?"block":"none",!i&&e.checked){const e={circuit_id:t.entities?.power||t.uuid};t.configEntryId&&(e.config_entry_id=t.configEntryId),this._callDomainService("clear_circuit_threshold",e).catch(e=>{console.warn("SPAN Panel: monitoring update failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})})}});e.appendChild(i)}_createThresholdRow(e,t,i,s){const o=document.createElement("div");o.className="field-row";const r=document.createElement("span");r.className="field-label",r.textContent=e;const a=document.createElement("input");return a.type="number",a.min="0",a.max="200",a.value=String(i),a.dataset.role=`threshold-${t}`,a.addEventListener("input",()=>{this._debounce(`threshold-${t}`,u,()=>{const e=this.shadowRoot;if(!e)return;const t=e.querySelector('[data-role="threshold-continuous"]'),i=e.querySelector('[data-role="threshold-spike"]'),o=e.querySelector('[data-role="threshold-window-m"]'),r=e.querySelector('[data-role="threshold-cooldown-m"]'),a={circuit_id:s.entities?.power||s.uuid,continuous_threshold_pct:t?Number(t.value):void 0,spike_threshold_pct:i?Number(i.value):void 0,window_duration_m:o?Number(o.value):void 0,cooldown_duration_m:r?Number(r.value):void 0};s.configEntryId&&(a.config_entry_id=s.configEntryId),this._callDomainService("set_circuit_threshold",a).catch(e=>{console.warn("SPAN Panel: monitoring update failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})})})}),o.appendChild(r),o.appendChild(a),o}_createDurationRow(e,t,i,s,o,r,a,l=!1){const c=document.createElement("div");c.className="field-row";const d=document.createElement("span");d.className="field-label",d.textContent=e;const h=document.createElement("div"),p=document.createElement("input");p.type="number",p.min=String(s),p.max=String(o),p.value=String(i),p.dataset.role=`threshold-${t}`,l&&(p.disabled=!0);const g=document.createElement("span");return g.textContent=r,h.appendChild(p),h.appendChild(g),l||p.addEventListener("input",()=>{this._debounce(`threshold-${t}`,u,()=>{const e=this.shadowRoot;if(!e)return;const t=e.querySelector('[data-role="threshold-continuous"]'),i=e.querySelector('[data-role="threshold-spike"]'),s=e.querySelector('[data-role="threshold-window-m"]'),o={circuit_id:a.uuid,continuous_threshold_pct:t?Number(t.value):void 0,spike_threshold_pct:i?Number(i.value):void 0,window_duration_m:s?Number(s.value):void 0};a.configEntryId&&(o.config_entry_id=a.configEntryId),this._callDomainService("set_circuit_threshold",o).catch(e=>{console.warn("SPAN Panel: monitoring update failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})})})}),c.appendChild(d),c.appendChild(h),c}_updateLiveState(){if(!this._config||this._config.panelMode)return;const e=this._config;if(!e.subDeviceMode&&!e.favoritesMode){if(e.entities?.switch){const t=this.shadowRoot?.querySelector('[data-role="relay-toggle"]');if(t){const n=this._hass?.states?.[e.entities.switch]?.state;"on"===n?t.setAttribute("checked",""):t.removeAttribute("checked")}}if(e.entities?.select){const t=this.shadowRoot?.querySelector('[data-role="shedding-select"]');if(t){const n=this._hass?.states?.[e.entities.select]?.state||"";t.value=n}}}}_callService(e,t,n){return this._hass?Promise.resolve(this._hass.callService(e,t,n)):Promise.resolve()}_callDomainService(e,t){return this._hass?this._hass.callWS({type:"call_service",domain:a,service:e,service_data:t}):Promise.resolve()}_debounce(e,t,n){this._debounceTimers[e]&&clearTimeout(this._debounceTimers[e]),this._debounceTimers[e]=setTimeout(()=>{delete this._debounceTimers[e],n()},t)}}try{customElements.get("span-side-panel")||customElements.define("span-side-panel",Je)}catch{}let Xe=class extends Pe{constructor(){super(...arguments),this._store=null,this._unsub=null,this._errors=[]}set store(e){if(this._store===e)return;this._unsub?.(),this._unsub=null,this._store=e,this._errors=e.active;const t=e;this._unsub=e.subscribe(()=>{this._errors=t.active})}connectedCallback(){if(super.connectedCallback(),this._store&&!this._unsub){const e=this._store;this._errors=e.active,this._unsub=e.subscribe(()=>{this._errors=e.active})}}disconnectedCallback(){super.disconnectedCallback(),this._unsub?.(),this._unsub=null}render(){return 0===this._errors.length?de:le`${this._errors.map(e=>le`
+
+
+ ${e.message}
+ ${e.retryFn?le`e.retryFn()}>${n("error.retry")} `:de}
+
+ `)}`}_iconForLevel(e){switch(e){case"error":return"mdi:alert-circle";case"warning":return"mdi:alert";default:return"mdi:information"}}};async function Ze(e,t){const[n,i,s]=await Promise.all([e.callWS({type:"config/area_registry/list"}),e.callWS({type:"config/entity_registry/list"}),e.callWS({type:"config/device_registry/list"})]),o=new Map;for(const e of n)o.set(e.area_id,e.name);const r=new Map;for(const e of i)e.area_id&&r.set(e.entity_id,e.area_id);const a=new Map;for(const e of s)a.set(e.id,e.area_id);let l;if(t.device_id){const e=a.get(t.device_id);e&&(l=o.get(e))}for(const e of Object.values(t.circuits)){let t;for(const n of Object.values(e.entities)){if(!n)continue;const e=r.get(n);if(e){t=o.get(e);break}}t||(t=l),e.area=t}}async function Ye(e,t,i){if(!t)throw new Error(n("card.device_not_found"));const s={type:`${a}/panel_topology`,device_id:t},o=i?await i.callWS(e,s,{errorId:"fetch:topology"}):await e.callWS(s),r=o.panel_size??function(e){let t=0;for(const n of Object.values(e))if(n)for(const e of n.tabs)e>t&&(t=e);return t>0?t+t%2:0}(o.circuits);if(!r)throw new Error(n("card.topology_error"));const l={type:"config/device_registry/list"},c=i?await i.callWS(e,l,{errorId:"fetch:topology"}):await e.callWS(l),d=(h=c.find(e=>e.id===t),h?{id:h.id,name:h.name,name_by_user:h.name_by_user,config_entries:h.config_entries,identifiers:h.identifiers,via_device_id:h.via_device_id,sw_version:h.sw_version,model:h.model}:null);var h;return await Ze(e,o),{topology:o,panelDevice:d,panelSize:r}}function et(){return`\n ${Object.entries(f).filter(([e])=>"unknown"!==e).map(([,e])=>{const t=Oe(e.icon),n=Oe(e.color),i=Oe(e.label());let s;if(e.icon2){s=`
`}else if(e.textLabel){s=`
${Oe(e.textLabel)} `}else s=`
`;return`
${s}${i}
`}).join("")}\n
`}function tt(e,t,i){const s="current"===(t.chart_metric||"power"),o=!!e.panel_entities?.site_power,r=!!e.panel_entities?.dsm_state,a=!!e.panel_entities?.current_power,l=!!e.panel_entities?.feedthrough_power,c=!!e.panel_entities?.pv_power,d=!!e.panel_entities?.battery_level;return`\n \n ${o?`\n
\n
${n("header.site")} \n
\n 0 \n ${s?"A":"kW"} \n
\n
`:""}\n ${r?`\n
\n
${n("header.grid")} \n
\n -- \n
\n
`:""}\n ${a?`\n
\n
${n("header.upstream")} \n
\n -- \n ${s?"A":"kW"} \n
\n
`:""}\n ${l?`\n
\n
${n("header.downstream")} \n
\n -- \n ${s?"A":"kW"} \n
\n
`:""}\n ${c?`\n
\n
${n("header.solar")} \n
\n -- \n ${s?"A":"kW"} \n
\n
`:""}\n ${d?`\n
\n
${n("header.battery")} \n
\n — \n % \n
\n
`:""}\n
\n `}function nt(e,t,i={}){const s=Oe(e.device_name||n("header.default_name")),o=Oe(e.serial||""),r=Oe(e.firmware||""),a="current"===(t.chart_metric||"power"),l=!1!==i.showSwitches;return`\n \n `}Xe.styles=C`
+ :host {
+ display: block;
+ }
+ .banner-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ font-size: 13px;
+ line-height: 1.4;
+ }
+ .banner-row + .banner-row {
+ border-top: 1px solid rgba(128, 128, 128, 0.2);
+ }
+ .banner-row.level-error {
+ background: color-mix(in srgb, var(--error-color, #db4437) 15%, transparent);
+ color: var(--error-color, #db4437);
+ }
+ .banner-row.level-warning {
+ background: color-mix(in srgb, var(--warning-color, #ff9800) 15%, transparent);
+ color: var(--warning-color, #ff9800);
+ }
+ .banner-row.level-info {
+ background: color-mix(in srgb, var(--info-color, #4285f4) 15%, transparent);
+ color: var(--info-color, #4285f4);
+ }
+ .icon {
+ flex-shrink: 0;
+ width: 18px;
+ height: 18px;
+ --mdc-icon-size: 18px;
+ }
+ .message {
+ flex: 1;
+ min-width: 0;
+ }
+ .retry-btn {
+ flex-shrink: 0;
+ background: none;
+ border: 1px solid currentColor;
+ border-radius: 4px;
+ color: inherit;
+ cursor: pointer;
+ font-size: 12px;
+ padding: 2px 8px;
+ }
+ .retry-btn:hover {
+ opacity: 0.8;
+ }
+ `,m([Me()],Xe.prototype,"_errors",void 0),Xe=m([Ee("span-error-banner")],Xe);const it=g.power;function st(e){return it.unit(e)}function ot(e){return(e<0?"-":"")+it.format(e)}function rt(e){return(Math.abs(e)/1e3).toFixed(1)}function at(e){return Math.ceil(e/2)}function lt(e){return e%2==0?1:0}function ct(e){if(2!==e.length)return null;const[t,n]=[Math.min(...e),Math.max(...e)];return at(t)===at(n)?"row-span":lt(t)===lt(n)?"col-span":"row-span"}function dt(e){const t=e.chart_metric??s;return g[t]??g[s]}function ht(e,t){const n=function(e){return dt(e).entityRole}(t);return e.entities?.[n]??e.entities?.power??null}class pt{constructor(){this._status=null,this._lastFetch=0,this._inflight=null,this._generation=0,this._errorStore=null,this._retry=null}get errorStore(){return this._errorStore}set errorStore(e){this._errorStore=e,this._retry=e?new We(e):null}async fetch(e,t){const i=Date.now();if(this._inflight&&this._inflight.gen===this._generation)return this._inflight.promise;if(this._status&&i-this._lastFetch<3e4)return this._status;const s=this._generation,o=(async()=>{try{const i={};t&&(i.config_entry_id=t);const o={type:"call_service",domain:a,service:"get_monitoring_status",service_data:i,return_response:!0},r=this._retry?await this._retry.callWS(e,o,{errorId:"fetch:monitoring",errorMessage:n("error.monitoring_failed")}):await e.callWS(o),l=r?.response??null;return s===this._generation&&(this._status=l,this._lastFetch=Date.now()),l}catch(e){return console.warn("SPAN Panel: monitoring status fetch failed",e),s===this._generation&&(this._status=null),this._retry||this._errorStore?.add({key:"fetch:monitoring",level:"warning",message:n("error.monitoring_failed"),persistent:!1}),null}finally{this._inflight?.gen===s&&(this._inflight=null)}})();return this._inflight={gen:s,promise:o},o}invalidate(){this._lastFetch=0,this._generation++}get status(){return this._status}clear(){this._status=null,this._lastFetch=0,this._generation++}}class ut{constructor(){this._caches=new Map,this._errorStore=null}get errorStore(){return this._errorStore}set errorStore(e){this._errorStore=e;for(const t of this._caches.values())t.errorStore=e}async fetchOne(e,t){let n=this._caches.get(t);return n||(n=new pt,n.errorStore=this._errorStore,this._caches.set(t,n)),n.fetch(e,t)}invalidate(){for(const e of this._caches.values())e.invalidate()}clear(){this._caches.clear()}}function gt(e,t){return e?.circuits?e.circuits[t]??null:null}function _t(e){return!!e&&void 0!==e.continuous_threshold_pct}function ft(e,t,n,i){const s=[];return n||s.push("circuit-off"),i&&s.push("circuit-producer"),function(e){return!!e&&null!=e.over_threshold_since}(t)&&s.push("circuit-alert"),_t(t)&&s.push("circuit-custom-monitoring"),s.join(" ")}function vt(e,t,i,s,o,r,a,d,h,p=!1){const u=t.entities?.power,g=u?r.states[u]:null,_=g&&parseFloat(g.state)||0,m=t.device_type===c||_<0,b=t.entities?.switch,y=b?r.states[b]:null,w=y?"on"===y.state:(g?.attributes?.relay_state||t.relay_state)===l,x=t.breaker_rating_a,S=x?`${Math.round(x)}A`:"",$=Oe(t.name||n("grid.unknown")),C=dt(a);let P;if("current"===C.entityRole){const e=t.entities?.current,n=e?r.states[e]:null,i=n&&parseFloat(n.state)||0;P=`${C.format(i)} A `}else P=`${ot(_)} ${st(_)} `;const k=h||"unknown";let E="";if("unknown"!==k){const e=f[k]??f.unknown??{icon:"mdi:help",color:"#999",label:()=>"Unknown"},t=Oe(e.label()),n=Oe(e.icon),i=Oe(e.color);if(e.icon2){E=`\n \n \n `}else if(e.textLabel){E=`\n \n ${Oe(e.textLabel)} \n `}else E=` `}const z=d&&_t(d)?v:"#555",A=`\n \n `;let N="",M=d?.utilization_pct??null;if(null==M&&t.breaker_rating_a){const e=t.entities?.current,n=e?r.states[e]:null,i=n?Math.abs(parseFloat(n.state)||0):0;M=Math.round(i/t.breaker_rating_a*1e3)/10}if(null!=M){N=`=80?"utilization-warning":"utilization-normal"}">${Math.round(M)}% `}return`\n \n \n
\n ${E}\n ${A}\n
\n
\n
\n `}function mt(e,t){return`\n \n — \n
\n `}const bt={names:["power","battery power"],suffixes:["_power"]},yt={names:["battery level","battery percentage"],suffixes:["_battery_level","_battery_percentage"]},wt={names:["state of energy"],suffixes:["_soe_kwh"]},xt={names:["nameplate capacity"],suffixes:["_nameplate_capacity"]};function St(e,t){if(!e.entities)return null;for(const[n,i]of Object.entries(e.entities)){if("sensor"!==i.domain)continue;const e=(i.original_name??"").toLowerCase();if(t.names.some(t=>e===t))return n;if(i.unique_id&&t.suffixes.some(e=>i.unique_id.endsWith(e)))return n}return null}function $t(e){return St(e,bt)}function Ct(e){return St(e,yt)}function Pt(e){return St(e,wt)}function kt(e){return St(e,xt)}function Et(e,t,i){const s=!1!==i.show_battery,o=!1!==i.show_evse;if(!e.sub_devices)return"";const r=Object.entries(e.sub_devices).filter(([,e])=>!(e.type===d&&!s)&&!(e.type===h&&!o));if(0===r.length)return"";const a=r.filter(([,e])=>e.type===h).length;let l=0,c="";for(const[e,s]of r){const o=s.type===h?n("subdevice.ev_charger"):s.type===d?n("subdevice.battery"):n("subdevice.fallback"),r=$t(s),p=r?t.states[r]:void 0,u=p&&parseFloat(p.state)||0,g=s.type===d,_=s.type===h,f=g?Ct(s):null,v=g?Pt(s):null,m=g?kt(s):null,b=zt(s,t,i,new Set([r,f,v,m].filter(e=>null!==e))),y=At(e,s,g,r,f,v);let w="";g?w="sub-device-bess":_&&(l++,l===a&&a%2==1&&(w="sub-device-full")),c+=`\n \n \n ${y}\n ${b}\n
\n `}return c}function zt(e,t,n,i){const s=n.visible_sub_entities||{};let o="";if(!e.entities)return o;for(const[n,r]of Object.entries(e.entities)){if(i.has(n))continue;if(!0!==s[n])continue;const a=t.states[n];if(!a)continue;let l=r.original_name||a.attributes.friendly_name||n;const c=e.name||"";let d;if(l.startsWith(c+" ")&&(l=l.slice(c.length+1)),t.formatEntityState)d=t.formatEntityState(a);else{d=a.state;const e=a.attributes.unit_of_measurement||"";e&&(d+=" "+e)}if("Wh"===(a.attributes.unit_of_measurement||"")){const e=parseFloat(a.state);isNaN(e)||(d=(e/1e3).toFixed(1)+" kWh")}o+=`\n \n ${Oe(l)}: \n ${Oe(d)} \n
\n `}return o}function At(e,t,i,s,o,r){if(i){const t=[{key:`${p}${e}_soc`,title:n("subdevice.soc"),available:!!o},{key:`${p}${e}_soe`,title:n("subdevice.soe"),available:!!r},{key:`${p}${e}_power`,title:n("subdevice.power"),available:!!s}].filter(e=>e.available);return`\n \n ${t.map(e=>`\n
\n `).join("")}\n
\n `}return s?`
`:""}function Nt(e){const t=void 0!==e.history_days||void 0!==e.history_hours||void 0!==e.history_minutes,n=60*(60*(24*(t&&parseInt(String(e.history_days))||0)+(t&&parseInt(String(e.history_hours))||0))+(t?parseInt(String(e.history_minutes))||0:5))*1e3;return Math.max(n,6e4)}function Mt(e){const t=r[e];return t?t.ms:r[o].ms}function It(e){const t=e/1e3;return t<=600?Math.ceil(t):Math.min(5e3,Math.ceil(t/5))}function Tt(e){return Math.max(500,Math.floor(e/5e3))}function Dt(e,t,n,i,s,o){e.has(t)||e.set(t,[]);const r=e.get(t);r.push({time:i,value:n});const a=r.findIndex(e=>e.time>=s);a>0?r.splice(0,a):-1===a&&(r.length=0),r.length>o&&r.splice(0,r.length-o)}function Ft(e,t,n=500){if(0===e.length)return e;e.sort((e,t)=>e.time-t.time);const i=[e[0]];for(let t=1;t=n&&i.push(e[t]);return i.length>t&&i.splice(0,i.length-t),i}async function Lt(e,t,n,i,s){const o=new Date(Date.now()-i).toISOString(),r=i/36e5>72?"hour":"5minute",a=await e.callWS({type:"recorder/statistics_during_period",start_time:o,statistic_ids:t,period:r,types:["mean"]});for(const[e,t]of Object.entries(a)){const i=n.get(e);if(!i||!t)continue;const o=[];for(const e of t){const t=e.mean;if(null==t||!Number.isFinite(t))continue;const n=e.start;n>0&&o.push({time:n,value:t})}if(o.length>0){const e=s.get(i)||[],t=[...o,...e];t.sort((e,t)=>e.time-t.time),s.set(i,t)}}}async function Ht(e,t,n,i,s){const o=new Date(Date.now()-i).toISOString(),r=await e.callWS({type:"history/history_during_period",start_time:o,entity_ids:t,minimal_response:!0,significant_changes_only:!0,no_attributes:!0}),a=It(i),l=Tt(i);for(const[e,t]of Object.entries(r)){const i=n.get(e);if(!i||!t)continue;const o=[];for(const e of t){const t=parseFloat(e.s);if(!Number.isFinite(t))continue;const n=1e3*(e.lu||e.lc||0);n>0&&o.push({time:n,value:t})}if(o.length>0){const e=s.get(i)||[],t=[...o,...e];s.set(i,Ft(t,a,l))}}}function Ot(e){if(!e.sub_devices)return[];const t=[];for(const[n,i]of Object.entries(e.sub_devices)){const e={power:$t(i)};i.type===d&&(e.soc=Ct(i),e.soe=Pt(i));for(const[i,s]of Object.entries(e))s&&t.push({entityId:s,key:`${p}${n}_${i}`,devId:n})}return t}async function Rt(e,t,n,i,s,o){if(!t||!e)return;const r=new Map;for(const[e,i]of Object.entries(t.circuits)){const t=ht(i,n);if(!t)continue;let o;o=s&&s.has(e)?Mt(s.get(e)):Nt(n),r.has(o)||r.set(o,{entityIds:[],uuidByEntity:new Map});const a=r.get(o);a.entityIds.push(t),a.uuidByEntity.set(t,e)}for(const{entityId:e,key:i,devId:s}of Ot(t)){let t;t=o&&o.has(s)?Mt(o.get(s)):Nt(n),r.has(t)||r.set(t,{entityIds:[],uuidByEntity:new Map});const a=r.get(t);a.entityIds.push(e),a.uuidByEntity.set(e,i)}const a=[];for(const[t,n]of r){if(0===n.entityIds.length)continue;t>2592e5?a.push(Lt(e,n.entityIds,n.uuidByEntity,t,i)):a.push(Ht(e,n.entityIds,n.uuidByEntity,t,i))}await Promise.all(a)}function qt(e,t,n,i,o,r,a,l,c){const{options:d,series:h}=function(e,t,n,i,o,r=!1){n||(n=g[s]);const a=i?"140, 160, 220":"77, 217, 175",l=`rgb(${a})`,c=Date.now(),d=c-t,h=void 0!==n.fixedMin&&void 0!==n.fixedMax,p=(e??[]).filter(e=>e.time>=d).map(e=>[e.time,Math.abs(e.value)]),u=[{type:"line",data:p,showSymbol:!1,smooth:!1,...r?{}:{step:"end"},lineStyle:{width:1.5,color:l},areaStyle:{color:{type:"linear",x:0,y:0,x2:0,y2:1,colorStops:[{offset:0,color:`rgba(${a}, 0.18)`},{offset:1,color:`rgba(${a}, 0.18)`}]}},itemStyle:{color:l}}],_=p.length>0?function(e){let t=0;for(const n of e)n[1]>t&&(t=n[1]);return t}(p):0,f={type:"value",splitNumber:4,axisLabel:{fontSize:10,formatter:_<10?e=>0===e?"0":e.toFixed(1):e=>n.format(e)},splitLine:{lineStyle:{opacity:.15}}};h?(f.min=n.fixedMin,f.max=n.fixedMax):_<1&&(f.min=0,f.max=1),o&&"current"===n.entityRole&&(f.min=0,f.max=Math.ceil(1.25*o),u.push({type:"line",data:[[d,.8*o],[c,.8*o]],showSymbol:!1,lineStyle:{width:1,color:"rgba(255, 200, 40, 0.6)",type:"dashed"},itemStyle:{color:"transparent"},tooltip:{show:!1}}),u.push({type:"line",data:[[d,o],[c,o]],showSymbol:!1,lineStyle:{width:1.5,color:"rgba(255, 60, 60, 0.7)",type:"solid"},itemStyle:{color:"transparent"},tooltip:{show:!1}}));const v={xAxis:{type:"time",min:d,max:c,axisLabel:{fontSize:10},splitLine:{show:!1}},yAxis:f,grid:{top:8,right:4,bottom:0,left:0,containLabel:!0},tooltip:{trigger:"axis",axisPointer:{type:"line",lineStyle:{type:"dashed"}},formatter:e=>{if(!e||0===e.length)return"";const t=e[0],i=new Date(t.value[0]).toLocaleString(void 0,{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit",second:"2-digit"}),s=parseFloat(t.value[1].toFixed(2));return`${i}${n.format(s)} ${n.unit(s)}
`}},animation:!1};return{options:v,series:u}}(n,i,o,r,l,c),p=a??120;e.style.minHeight=p+"px";let u=e.querySelector("ha-chart-base");u||(u=document.createElement("ha-chart-base"),u.style.display="block",u.style.width="100%",u.hass=t,e.innerHTML="",e.appendChild(u));const _=e.clientHeight;u.height=(_>0?_:p)+"px",u.hass=t,u.options=d,u.data=h}function jt(e){return"function"==typeof globalThis.CSS?.escape?CSS.escape(e):e.replace(/["\\]/g,"\\$&")}function Ut(e,t,n,i,s){const o="current"===(i.chart_metric||"power"),r=e.querySelector(".stat-consumption .stat-value"),a=e.querySelector(".stat-consumption .stat-unit");if(o){const e=n.panel_entities?.site_power,i=e?t.states[e]:null,s=i?parseFloat(i.attributes?.amperage):NaN;r&&(r.textContent=Number.isFinite(s)?Math.abs(s).toFixed(1):"--"),a&&(a.textContent="A")}else{let e=s;const i=n.panel_entities?.site_power;if(i){const n=t.states[i];n&&(e=Math.abs(parseFloat(n.state)||0))}r&&(r.textContent=rt(e)),a&&(a.textContent="kW")}const l=e.querySelector(".stat-upstream .stat-value"),c=e.querySelector(".stat-upstream .stat-unit");if(l){const e=n.panel_entities?.current_power,i=e?t.states[e]:null;if(o){const e=i?parseFloat(i.attributes?.amperage):NaN;l.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",c&&(c.textContent="A")}else{const e=i?Math.abs(parseFloat(i.state)||0):0;l.textContent=rt(e),c&&(c.textContent="kW")}}const d=e.querySelector(".stat-downstream .stat-value"),h=e.querySelector(".stat-downstream .stat-unit");if(d){const e=n.panel_entities?.feedthrough_power,i=e?t.states[e]:null;if(o){const e=i?parseFloat(i.attributes?.amperage):NaN;d.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",h&&(h.textContent="A")}else{const e=i?Math.abs(parseFloat(i.state)||0):0;d.textContent=rt(e),h&&(h.textContent="kW")}}const p=e.querySelector(".stat-solar .stat-value"),u=e.querySelector(".stat-solar .stat-unit");if(p){const e=n.panel_entities?.pv_power,i=e?t.states[e]:null;if(o){const e=i?parseFloat(i.attributes?.amperage):NaN;p.textContent=Number.isFinite(e)?Math.abs(e).toFixed(1):"--",u&&(u.textContent="A")}else{if(i){const e=Math.abs(parseFloat(i.state)||0);p.textContent=rt(e)}else p.textContent="--";u&&(u.textContent="kW")}}const g=e.querySelector(".stat-battery .stat-value");if(g){const e=n.panel_entities?.battery_level,i=e?t.states[e]:null;i&&(g.textContent=`${Math.round(parseFloat(i.state)||0)}`)}const _=e.querySelector(".stat-grid-state .stat-value");if(_){const e=n.panel_entities?.dsm_state,i=e?t.states[e]:null;_.textContent=i?t.formatEntityState?.(i)||i.state:"--"}}function Wt(e,t,i,s,o,r){if(!e||!i||!t)return;const a=Nt(s);let d=0;for(const[,e]of Object.entries(i.circuits)){const n=e.entities?.power;if(!n)continue;const i=t.states[n],s=i&&parseFloat(i.state)||0;e.device_type!==c&&(d+=Math.abs(s))}!function(e,t,n,i,s){const o=e.querySelector(".panel-stats");o&&Ut(o,t,n,i,s)}(e,t,i,s,d);const h=dt(s),p="current"===h.entityRole;for(const[s,d]of Object.entries(i.circuits)){const i=e.querySelector(`.circuit-slot[data-uuid="${jt(s)}"]`);if(!i)continue;const u=d.entities?.power,g=u?t.states[u]:null,_=g&&parseFloat(g.state)||0,v=d.device_type===c||_<0,m=d.entities?.switch,b=m?t.states[m]:null,y=b?"on"===b.state:(g?.attributes?.relay_state||d.relay_state)===l,w=i.querySelector(".power-value");if(w)if(p){const e=d.entities?.current,n=e?t.states[e]:null,i=n&&parseFloat(n.state)||0;w.innerHTML=`${h.format(i)} A `}else w.innerHTML=`${ot(_)} ${st(_)} `;const x=i.querySelector(".toggle-pill");if(x){x.className="toggle-pill "+(y?"toggle-on":"toggle-off");const e=x.querySelector(".toggle-label");e&&(e.textContent=n(y?"grid.on":"grid.off"))}let S;if(i.classList.toggle("circuit-off",!y),i.classList.toggle("circuit-producer",v),d.always_on)S="always_on";else{const e=d.entities?.select,n=e?t.states[e]:null;S=n?n.state:"unknown"}const $=f[S]??f.unknown,C=i.querySelector(".shedding-icon");C&&(C.setAttribute("icon",$.icon),C.style.color=$.color,C.title=$.label());const P=i.querySelector(".shedding-icon-secondary");P&&($.icon2?(P.setAttribute("icon",$.icon2),P.style.color=$.color,P.style.display=""):P.style.display="none");const k=i.querySelector(".shedding-label");k&&($.textLabel?(k.textContent=$.textLabel,k.style.color=$.color,k.style.display=""):k.style.display="none");const E=i.querySelector(".chart-container");if(E){const e=o.get(s)||[],n=i.classList.contains("circuit-col-span")?200:100,l=r?.has(s)?Mt(r.get(s)):a,p=d.device_type===c;qt(E,t,e,l,h,v,n,d.breaker_rating_a??void 0,p)}}}class Gt{get errorStore(){return this._errorStore}set errorStore(e){this._errorStore=e,this._retry=e?new We(e):null}constructor(){this._errorStore=null,this._retry=null,this._settings=null,this._lastFetch=0,this._fetching=!1}async fetch(e,t){const i=Date.now();if(this._fetching)return this._settings;if(this._settings&&i-this._lastFetch<3e4)return this._settings;this._fetching=!0;try{const i={};t&&(i.config_entry_id=t);const s={type:"call_service",domain:a,service:"get_graph_settings",service_data:i,return_response:!0},o=this._retry?await this._retry.callWS(e,s,{errorId:"fetch:graph_settings",errorMessage:n("error.graph_settings_failed")}):await e.callWS(s);this._settings=o?.response??null,this._lastFetch=Date.now()}catch(e){console.warn("SPAN Panel: graph settings fetch failed",e),this._settings=null,this._retry||this._errorStore?.add({key:"fetch:graph_settings",level:"warning",message:n("error.graph_settings_failed"),persistent:!1})}finally{this._fetching=!1}return this._settings}invalidate(){this._lastFetch=0}get settings(){return this._settings}clear(){this._settings=null,this._lastFetch=0}}function Vt(e,t){if(!e)return o;const n=e.circuits?.[t];return n?.has_override?n.horizon:e.global_horizon??o}function Bt(e,t){if(!e)return o;const n=e.sub_devices?.[t];return n?.has_override?n.horizon:e.global_horizon??o}class Qt{constructor(){this.powerHistory=new Map,this.horizonMap=new Map,this.subDeviceHorizonMap=new Map,this.monitoringCache=new pt,this.monitoringMultiCache=new ut,this.graphSettingsCache=new Gt,this._errorStore=null,this._hass=null,this._topology=null,this._config=null,this._configEntryId=null,this._favRefs=null,this._perPanelInfo=new Map,this._panelFavorites=null,this._showMonitoring=!1,this._updateInterval=null,this._recorderRefreshInterval=null,this._resizeObserver=null,this._lastWidth=0,this._resizeDebounce=null}get errorStore(){return this._errorStore}set errorStore(e){this._errorStore=e,this.monitoringCache.errorStore=e,this.graphSettingsCache.errorStore=e,this.monitoringMultiCache.errorStore=e}get hass(){return this._hass}set hass(e){this._hass=e}get topology(){return this._topology}get config(){return this._config}set showMonitoring(e){this._showMonitoring=e}init(e,t,n,i){this._topology=e,this._config=t,this._hass=n,this._configEntryId=i}setFavoriteRefs(e){this._favRefs=e}clearFavoriteRefs(){this._favRefs=null}setPanelFavorites(e){this._panelFavorites=e}setFavoritesPerPanelInfo(e){this._perPanelInfo=e??new Map}get _inFavoritesView(){return null!==this._favRefs}setConfig(e){this._config=e}buildHorizonMaps(e){if(this.horizonMap.clear(),this.subDeviceHorizonMap.clear(),e&&this._topology?.circuits)for(const t of Object.keys(this._topology.circuits))this.horizonMap.set(t,Vt(e,t));if(e&&this._topology?.sub_devices)for(const t of Object.keys(this._topology.sub_devices))this.subDeviceHorizonMap.set(t,Bt(e,t))}async fetchAndBuildHorizonMaps(){try{this._favRefs?await this._buildFavoritesHorizonMaps():(await this.graphSettingsCache.fetch(this._hass,this._configEntryId),this.buildHorizonMaps(this.graphSettingsCache.settings))}catch(e){console.warn("SPAN Panel: graph settings fetch failed",e),this.graphSettingsCache.errorStore||this._errorStore?.add({key:"fetch:graph_settings",level:"warning",message:n("error.graph_settings_failed"),persistent:!1})}}async fetchMergedMonitoringStatus(e){if(!this._hass||0===e.length)return null;const t=this._hass;return function(e){let t=!1;const n={},i={};for(const s of e)s&&(t=!0,s.circuits&&Object.assign(n,s.circuits),s.mains&&Object.assign(i,s.mains));return t?{circuits:n,mains:i}:null}(await Promise.all(e.map(e=>this.monitoringMultiCache.fetchOne(t,e))))}async _buildFavoritesHorizonMaps(){if(!this._hass||!this._favRefs||!this._topology)return;const e=new Set;for(const t of Object.values(this._favRefs))t.configEntryId&&e.add(t.configEntryId);const t=new Map;await Promise.all(Array.from(e).map(async e=>{t.set(e,await this._fetchGraphSettingsFresh(e))})),this.horizonMap.clear(),this.subDeviceHorizonMap.clear();for(const e of Object.keys(this._topology.circuits)){const n=this._favRefs[e],i=n?.configEntryId?t.get(n.configEntryId)??null:null,s=n?.targetId??e;this.horizonMap.set(e,Vt(i,s))}if(this._topology.sub_devices)for(const e of Object.keys(this._topology.sub_devices)){const n=this._favRefs[e],i=n?.configEntryId?t.get(n.configEntryId)??null:null,s=n?.targetId??e;this.subDeviceHorizonMap.set(e,Bt(i,s))}}async loadHistory(){await Rt(this._hass,this._topology,this._config,this.powerHistory,this.horizonMap,this.subDeviceHorizonMap)}recordSamples(){if(!this._topology||!this._hass||!this._config)return;const e=Date.now();for(const[t,n]of Object.entries(this._topology.circuits)){const i=this.horizonMap.get(t)??o;if(!r[i]?.useRealtime)continue;const s=ht(n,this._config);if(!s)continue;const a=this._hass.states[s];if(!a)continue;const l=parseFloat(a.state);if(isNaN(l))continue;const c=Mt(i),d=It(c),h=Tt(c),p=e-c,u=this.powerHistory.get(t)??[];u.length>0&&e-u[u.length-1].time0&&e-u[u.length-1].time0&&this._topology)for(const{key:e,devId:t}of Ot(this._topology))i.has(t)&&s.add(e);const o=new Map;try{await Rt(this._hass,this._topology,this._config,o,t,i);for(const e of t.keys()){const t=o.get(e);t?this.powerHistory.set(e,t):this.powerHistory.delete(e)}for(const e of s){const t=o.get(e);t?this.powerHistory.set(e,t):this.powerHistory.delete(e)}this.updateDOM(e)}catch(e){console.warn("SPAN Panel: history refresh failed",e),this._errorStore?.add({key:"fetch:history",level:"warning",message:n("error.history_failed"),persistent:!1})}}updateDOM(e){this._hass&&this._topology&&this._config&&(Wt(e,this._hass,this._topology,this._config,this.powerHistory,this.horizonMap),function(e,t,n,i,s,o){if(!n.sub_devices)return;const r=Nt(i);for(const[i,a]of Object.entries(n.sub_devices)){const n=e.querySelector(`[data-subdev="${jt(i)}"]`);if(!n)continue;const l=$t(a);if(l){const e=t.states[l],i=e&&parseFloat(e.state)||0,s=n.querySelector(".sub-power-value");s&&(s.innerHTML=`${ot(i)} ${st(i)} `)}const c=n.querySelectorAll("[data-chart-key]");for(const e of c){const n=e.dataset.chartKey;if(!n)continue;const a=s.get(n)||[];let l=_.power;n.endsWith("_soc")?l=_.soc:n.endsWith("_soe")&&(l=_.soe);const c=!!e.closest(".bess-chart-col");qt(e,t,a,o?.has(i)?Mt(o.get(i)):r,l,!1,c?120:150,void 0,n.endsWith("_soc")||n.endsWith("_soe"))}for(const e of Object.keys(a.entities||{})){const i=n.querySelector(`[data-eid="${jt(e)}"]`);if(!i)continue;const s=t.states[e];if(s){let e;if(t.formatEntityState)e=t.formatEntityState(s);else{e=s.state;const t=s.attributes.unit_of_measurement||"";t&&(e+=" "+t)}if("Wh"===(s.attributes.unit_of_measurement||"")){const t=parseFloat(s.state);isNaN(t)||(e=(t/1e3).toFixed(1)+" kWh")}i.textContent=e}}}}(e,this._hass,this._topology,this._config,this.powerHistory,this.subDeviceHorizonMap))}async onGraphSettingsChanged(e){if(this._hass){this._favRefs?await this._buildFavoritesHorizonMaps():(this.graphSettingsCache.invalidate(),await this.graphSettingsCache.fetch(this._hass,this._configEntryId),this.buildHorizonMaps(this.graphSettingsCache.settings)),this.powerHistory.clear();try{await this.loadHistory()}catch{}this.updateDOM(e)}}onToggleClick(e,t){const i=e.target,s=i?.closest(".toggle-pill");if(!s)return;const o=t.querySelector(".slide-confirm");if(!o||!o.classList.contains("confirmed"))return;e.stopPropagation(),e.preventDefault();const r=s.closest("[data-uuid]");if(!r||!this._topology||!this._hass)return;const a=r.dataset.uuid;if(!a)return;const l=this._topology.circuits[a];if(!l)return;const c=l.entities?.switch;if(!c)return;const d=this._hass.states[c];if(!d)return void console.warn("SPAN Panel: switch entity not found:",c);const h="on"===d.state?"turn_off":"turn_on";this._hass.callService("switch",h,{},{entity_id:c}).catch(e=>{console.warn("SPAN Panel: switch service call failed",e),this._errorStore?.add({key:"service:relay",level:"error",message:n("error.relay_failed"),persistent:!1})})}async onGearClick(e,t){const n=e.target,i=n?.closest(".gear-icon");if(!i)return;const s=t.querySelector("span-side-panel");if(!s||!this._hass)return;if(s.hass=this._hass,s.errorStore=this.errorStore,i.classList.contains("panel-gear")){if(this._inFavoritesView){const e=await this._buildFavoritesSections();if(0===e.length)return;return void s.open({favoritesMode:!0,perPanelSections:e})}return await this.graphSettingsCache.fetch(this._hass,this._configEntryId),void s.open({panelMode:!0,topology:this._topology,graphSettings:this.graphSettingsCache.settings,showFavorites:null!==this._panelFavorites,favoritePanelDeviceId:this._panelFavorites?.panelDeviceId,favoriteCircuitUuids:this._panelFavorites?.circuitUuids,favoriteSubDeviceIds:this._panelFavorites?.subDeviceIds,configEntryId:this._configEntryId})}const r=i.dataset.uuid;if(r&&this._topology){const e=this._topology.circuits[r];if(e){const t=this._favRefs?.[r]??null,n=t&&"circuit"===t.kind?t.targetId:r,i=t?.configEntryId??this._configEntryId;let a,l;t?[a,l]=await Promise.all([this._fetchGraphSettingsFresh(i),this._fetchMonitoringStatusFresh(i)]):(await Promise.all([this.graphSettingsCache.fetch(this._hass,i),this.monitoringCache.fetch(this._hass,i)]),a=this.graphSettingsCache.settings,l=this.monitoringCache.status);const c=e.entities?.current??e.entities?.power,d=c?l?.circuits?.[c]??null:null,h=a?.global_horizon??o,p=a?.circuits?.[n],u=p?{...p,globalHorizon:h}:{horizon:h,has_override:!1,globalHorizon:h},g=t?.panelDeviceId??this._panelFavorites?.panelDeviceId,_=null!==t||(this._panelFavorites?.circuitUuids.has(n)??!1),f=this._inFavoritesView||null!==this._panelFavorites;return void s.open({...e,uuid:n,monitoringInfo:d,showMonitoring:this._showMonitoring,graphHorizonInfo:u,showFavorites:f,favoritePanelDeviceId:g,isFavorite:_,configEntryId:i})}}const a=i.dataset.subdevId;if(a&&this._topology?.sub_devices?.[a]){const e=this._topology.sub_devices[a],t=this._favRefs?.[a]??null,n=t&&"sub_device"===t.kind?t.targetId:a,i=t?.configEntryId??this._configEntryId;let r;t?r=await this._fetchGraphSettingsFresh(i):(await this.graphSettingsCache.fetch(this._hass,i),r=this.graphSettingsCache.settings);const l=r?.global_horizon??o,c=r?.sub_devices?.[n],d=c?{...c,globalHorizon:l}:{horizon:l,has_override:!1,globalHorizon:l},h=t?.panelDeviceId??this._panelFavorites?.panelDeviceId,p=null!==t||(this._panelFavorites?.subDeviceIds.has(n)??!1),u=this._inFavoritesView||null!==this._panelFavorites;s.open({subDeviceMode:!0,subDeviceId:n,name:e.name??n,deviceType:e.type??"",entities:e.entities,graphHorizonInfo:d,showFavorites:u,favoritePanelDeviceId:h,isFavorite:p,configEntryId:i})}}async _buildFavoritesSections(){if(!this._hass||!this._favRefs)return[];const e=function(e,t){const n=new Map;for(const i of Object.values(e)){if("circuit"!==i.kind)continue;const e=t.get(i.panelDeviceId);if(void 0===e)continue;let s=n.get(i.panelDeviceId);void 0===s&&(s={panelDeviceId:i.panelDeviceId,panelName:e.panelName,topology:e.topology,configEntryId:e.configEntryId,favoriteCircuitUuids:new Set},n.set(i.panelDeviceId,s)),s.favoriteCircuitUuids.add(i.targetId)}return Array.from(n.values()).sort((e,t)=>e.panelName.localeCompare(t.panelName))}(this._favRefs,this._perPanelInfo);if(0===e.length)return[];return await Promise.all(e.map(async e=>({panelDeviceId:e.panelDeviceId,panelName:e.panelName,topology:e.topology,graphSettings:await this._fetchGraphSettingsFresh(e.configEntryId),favoriteCircuitUuids:e.favoriteCircuitUuids,configEntryId:e.configEntryId})))}async _fetchGraphSettingsFresh(e){if(!this._hass)return null;try{const t={};e&&(t.config_entry_id=e);const i={type:"call_service",domain:a,service:"get_graph_settings",service_data:t,return_response:!0},s=this._errorStore?new We(this._errorStore):null,o=s?await s.callWS(this._hass,i,{errorId:"fetch:graph_settings",errorMessage:n("error.graph_settings_failed")}):await this._hass.callWS(i);return o?.response??null}catch(e){return console.warn("SPAN Panel: fresh graph settings fetch failed",e),null}}async _fetchMonitoringStatusFresh(e){if(!this._hass)return null;try{const t={};e&&(t.config_entry_id=e);const i={type:"call_service",domain:a,service:"get_monitoring_status",service_data:t,return_response:!0},s=this._errorStore?new We(this._errorStore):null,o=s?await s.callWS(this._hass,i,{errorId:"fetch:monitoring",errorMessage:n("error.monitoring_failed")}):await this._hass.callWS(i),r=o?.response;return r?{circuits:r.circuits,mains:r.mains}:null}catch(e){return console.warn("SPAN Panel: fresh monitoring status fetch failed",e),null}}bindSlideConfirm(e,t){const n=e.querySelector(".slide-confirm-knob"),i=e.querySelector(".slide-confirm-text");if(!n||!i)return;let s=!1,o=0,r=0;const a=t=>{e.classList.contains("confirmed")||(s=!0,o=t-n.offsetLeft,r=e.offsetWidth-n.offsetWidth-4,n.classList.remove("snapping"))},l=e=>{if(!s)return;const t=Math.max(2,Math.min(e-o,r));n.style.left=t+"px"},c=()=>{if(!s)return;s=!1;(n.offsetLeft-2)/r>=.9?(n.style.left=r+"px",e.classList.add("confirmed"),n.querySelector("ha-icon")?.setAttribute("icon","mdi:lock-open"),i.textContent=e.dataset.textOn??"",t&&t.classList.remove("switches-disabled")):(n.classList.add("snapping"),n.style.left="2px")};n.addEventListener("mousedown",e=>{e.preventDefault(),a(e.clientX)}),e.addEventListener("mousemove",e=>l(e.clientX)),e.addEventListener("mouseup",c),e.addEventListener("mouseleave",c),n.addEventListener("touchstart",e=>{e.preventDefault(),a(e.touches[0].clientX)},{passive:!1}),e.addEventListener("touchmove",e=>l(e.touches[0].clientX),{passive:!0}),e.addEventListener("touchend",c),e.addEventListener("touchcancel",c),e.addEventListener("click",()=>{e.classList.contains("confirmed")&&(e.classList.remove("confirmed"),n.classList.add("snapping"),n.style.left="2px",n.querySelector("ha-icon")?.setAttribute("icon","mdi:lock"),i.textContent=e.dataset.textOff??"",t&&t.classList.add("switches-disabled"))})}startIntervals(e,t){this._updateInterval=setInterval(()=>{this.recordSamples(),this.updateDOM(e),t&&t()},1e3),this._recorderRefreshInterval=setInterval(()=>{this.refreshRecorderData(e)},3e4)}stopIntervals(){this._updateInterval&&(clearInterval(this._updateInterval),this._updateInterval=null),this._recorderRefreshInterval&&(clearInterval(this._recorderRefreshInterval),this._recorderRefreshInterval=null),this.cleanupResizeObserver()}setupResizeObserver(e,t){this.cleanupResizeObserver(),t&&(this._lastWidth=t.clientWidth,this._resizeObserver=new ResizeObserver(t=>{const n=t[0];if(!n)return;const i=n.contentRect.width;Math.abs(i-this._lastWidth)<5||(this._lastWidth=i,this._resizeDebounce&&clearTimeout(this._resizeDebounce),this._resizeDebounce=setTimeout(()=>{for(const t of e.querySelectorAll(".chart-container")){const e=t.querySelector("ha-chart-base");e&&e.remove()}this.updateDOM(e)},150))}),this._resizeObserver.observe(t))}cleanupResizeObserver(){this._resizeObserver&&(this._resizeObserver.disconnect(),this._resizeObserver=null),this._resizeDebounce&&(clearTimeout(this._resizeDebounce),this._resizeDebounce=null)}reset(){this.powerHistory.clear(),this.horizonMap.clear(),this.subDeviceHorizonMap.clear(),this.monitoringCache.clear(),this.monitoringMultiCache.clear(),this.graphSettingsCache.clear()}}const Kt='\n :host {\n --span-accent: var(--primary-color, #4dd9af);\n }\n\n ha-card {\n padding: 24px;\n background: var(--card-background-color, #1c1c1c);\n color: var(--primary-text-color, #e0e0e0);\n border-radius: var(--ha-card-border-radius, 12px);\n border: var(--ha-card-border-width, 1px) solid var(--ha-card-border-color, var(--divider-color, #333));\n box-shadow: var(--ha-card-box-shadow, none);\n }\n\n .panel-header {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n align-items: flex-start;\n gap: 8px 16px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .header-left { flex: 1 1 300px; min-width: 0; }\n .header-center { flex: 0 0 auto; }\n .header-right { flex: 0 1 auto; min-width: 0; }\n\n .panel-identity {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 8px 12px;\n margin-bottom: 12px;\n }\n\n .panel-title {\n font-size: 1.8em;\n font-weight: 700;\n margin: 0;\n color: var(--primary-text-color, #fff);\n }\n\n .panel-serial {\n font-size: 0.85em;\n color: var(--secondary-text-color, #999);\n font-family: monospace;\n }\n\n .panel-stats {\n display: flex;\n flex-wrap: wrap;\n gap: 16px 32px;\n }\n\n /* Favorites view header: gear + slide-to-arm + right-anchored legend/W-A cluster. */\n .favorites-summary {\n padding: 8px 24px;\n border-bottom: 1px solid var(--divider-color, #e0e0e0);\n display: flex;\n align-items: center;\n gap: 12px;\n }\n /* Override the generic .gear-icon { margin-left: auto } rule so the\n favorites gear stays flush-left instead of floating to the right of\n the flex row (same idea as .panel-identity .panel-gear does for\n real-panel headers). */\n .favorites-summary .favorites-gear {\n margin-left: 0;\n }\n /* Right-anchored cluster wrapping the shedding legend + W/A unit toggle.\n margin-left:auto moved here from .favorites-summary-unit-toggle so the\n legend and toggle cluster together, matching the real-panel header\n layout. */\n .favorites-summary-right {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 16px;\n }\n .favorites-subdevices-section {\n padding: 8px 16px 0;\n }\n\n /* Favorites view: responsive grid of per-contributing-panel status cards. */\n .favorites-panel-stats-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n gap: 12px;\n padding: 12px 24px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n .favorites-panel-card {\n background: var(--secondary-background-color, rgba(255, 255, 255, 0.04));\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n padding: 10px 14px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n .favorites-panel-card-title {\n font-size: 0.85em;\n font-weight: 600;\n color: var(--primary-text-color);\n opacity: 0.85;\n }\n .favorites-panel-card .panel-stats {\n gap: 10px 20px;\n }\n .favorites-panel-card .stat-value {\n font-size: 1.15em;\n }\n\n .stat { display: flex; flex-direction: column; }\n .stat-label { font-size: 0.8em; color: var(--secondary-text-color, #999); margin-bottom: 2px; }\n .stat-row { display: flex; align-items: baseline; gap: 2px; }\n .stat-value { font-size: 1.5em; font-weight: 700; color: var(--primary-text-color, #fff); }\n .stat-unit { font-size: 0.7em; font-weight: 400; color: var(--secondary-text-color, #999); }\n\n .header-right { display: flex; flex-direction: column; align-items: flex-end; gap: 8px; padding-top: 8px; }\n .header-right-top { display: flex; gap: 20px; align-items: center; }\n .meta-item { font-size: 0.8em; color: var(--secondary-text-color, #999); }\n\n .shedding-legend { display: flex; gap: 12px; flex-wrap: wrap; justify-content: flex-end; }\n .shedding-legend-item { display: inline-flex; align-items: center; gap: 3px; }\n .shedding-legend-item ha-icon { --mdc-icon-size: 16px; }\n .shedding-legend-secondary { --mdc-icon-size: 12px; opacity: 0.8; }\n .shedding-legend-text { font-size: 9px; font-weight: 600; }\n .shedding-legend-label { font-size: 0.7em; color: var(--secondary-text-color, #999); }\n\n .panel-gear {\n background: none;\n border: none;\n cursor: pointer;\n color: var(--secondary-text-color);\n opacity: 0.6;\n padding: 4px;\n margin-left: 8px;\n vertical-align: middle;\n }\n .panel-gear:hover { opacity: 1; }\n .header-center {\n display: flex;\n align-items: flex-start;\n justify-content: center;\n padding-top: 8px;\n }\n .panel-identity .panel-gear {\n margin-left: 0;\n }\n .slide-confirm {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 160px;\n height: 28px;\n border-radius: 14px;\n background: color-mix(in srgb, var(--primary-color, #4dd9af) 20%, var(--secondary-background-color, #333));\n vertical-align: middle;\n overflow: hidden;\n user-select: none;\n touch-action: none;\n }\n .slide-confirm-text {\n position: absolute;\n width: 100%;\n text-align: center;\n font-size: 0.65em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n pointer-events: none;\n z-index: 0;\n }\n .slide-confirm-knob {\n position: absolute;\n left: 2px;\n top: 2px;\n width: 24px;\n height: 24px;\n border-radius: 50%;\n background: var(--secondary-text-color, #666);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: grab;\n z-index: 1;\n transition: none;\n }\n .slide-confirm-knob ha-icon {\n --mdc-icon-size: 14px;\n color: var(--card-background-color, #1c1c1c);\n }\n .slide-confirm-knob.snapping {\n transition: left 0.25s ease;\n }\n .slide-confirm.confirmed {\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n }\n .slide-confirm.confirmed .slide-confirm-text {\n color: var(--state-active-color, var(--span-accent));\n }\n .slide-confirm.confirmed .slide-confirm-knob {\n background: var(--state-active-color, var(--span-accent));\n }\n .switches-disabled .toggle-pill {\n opacity: 0.3;\n pointer-events: none;\n }\n .unit-toggle {\n display: inline-flex;\n background: var(--secondary-background-color, #333);\n border-radius: 6px;\n overflow: hidden;\n margin-left: 8px;\n }\n .unit-btn {\n padding: 4px 10px;\n border: none;\n background: none;\n color: var(--secondary-text-color);\n font-size: 0.75em;\n font-weight: 600;\n cursor: pointer;\n }\n .unit-btn.unit-active {\n background: var(--primary-color, #4dd9af);\n color: var(--text-primary-color, #000);\n }\n\n .monitoring-summary {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 6px 16px;\n font-size: 0.8em;\n background: rgba(76, 175, 80, 0.1);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n }\n .monitoring-active { color: #4caf50; }\n .monitoring-counts { display: flex; gap: 12px; }\n .count-warning { color: #ff9800; }\n .count-alert { color: #f44336; }\n .count-overrides { color: var(--secondary-text-color); }\n\n .panel-grid {\n display: grid;\n grid-template-columns: 28px 1fr 1fr 28px;\n gap: 8px;\n align-items: stretch;\n }\n\n .tab-label {\n display: flex;\n align-items: center;\n font-size: 0.85em;\n font-weight: 600;\n color: var(--secondary-text-color, #999);\n user-select: none;\n }\n .tab-left { justify-content: flex-start; }\n .tab-right { justify-content: flex-end; }\n\n .circuit-slot {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px 20px;\n min-height: 140px;\n transition: opacity 0.3s;\n position: relative;\n overflow: hidden;\n }\n\n .circuit-col-span { min-height: 280px; }\n .circuit-row-span { border-left: 3px solid var(--span-accent); }\n .circuit-off .circuit-name,\n .circuit-off .breaker-badge,\n .circuit-off .power-value,\n .circuit-off .chart-container { opacity: 0.35; }\n .circuit-off .toggle-pill,\n .circuit-off .gear-icon { opacity: 1; }\n\n .circuit-empty {\n opacity: 0.2;\n min-height: 60px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-style: dashed;\n }\n .empty-label { color: var(--secondary-text-color, #999); font-size: 0.85em; }\n\n .circuit-header {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n margin-bottom: 6px;\n gap: 8px;\n }\n\n .circuit-info { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }\n\n .breaker-badge {\n background: color-mix(in srgb, var(--span-accent) 15%, transparent);\n color: var(--span-accent);\n font-size: 0.7em;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 4px;\n white-space: nowrap;\n border: 1px solid color-mix(in srgb, var(--span-accent) 25%, transparent);\n flex-shrink: 0;\n }\n\n .circuit-name {\n font-size: 0.9em;\n font-weight: 500;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n color: var(--primary-text-color, #e0e0e0);\n }\n\n .circuit-controls { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }\n\n .power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .power-value strong { font-weight: 700; font-size: 1.1em; }\n .power-unit { font-size: 0.8em; font-weight: 400; color: var(--secondary-text-color, #999); margin-left: 1px; }\n .circuit-producer .power-value strong { color: var(--info-color, #4fc3f7); }\n\n .toggle-pill {\n display: flex;\n align-items: center;\n gap: 3px;\n padding: 2px 4px;\n border-radius: 10px;\n cursor: pointer;\n font-size: 0.65em;\n font-weight: 600;\n transition: background 0.2s;\n user-select: none;\n min-width: 40px;\n }\n .toggle-on {\n padding-left: 6px;\n background: color-mix(in srgb, var(--state-active-color, var(--span-accent)) 25%, transparent);\n color: var(--state-active-color, var(--span-accent));\n }\n .toggle-off {\n padding-right: 6px;\n background: color-mix(in srgb, var(--secondary-text-color) 15%, transparent);\n color: var(--secondary-text-color, #999);\n }\n .toggle-knob {\n width: 14px;\n height: 14px;\n border-radius: 50%;\n transition: background 0.2s, margin 0.2s;\n }\n .toggle-on .toggle-knob {\n background: var(--state-active-color, var(--span-accent));\n margin-left: auto;\n }\n .toggle-off .toggle-knob {\n background: var(--secondary-text-color, #999);\n margin-right: auto;\n order: -1;\n }\n\n .circuit-status {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-top: 4px;\n padding: 0 4px;\n }\n .shedding-icon { opacity: 0.8; cursor: default; }\n .shedding-composite {\n display: inline-flex;\n align-items: center;\n gap: 2px;\n }\n .shedding-icon-secondary { opacity: 0.8; }\n .shedding-label {\n font-size: 10px;\n font-weight: 600;\n opacity: 0.8;\n }\n .gear-icon {\n background: none;\n border: none;\n cursor: pointer;\n padding: 2px;\n opacity: 0.6;\n transition: opacity 0.2s;\n margin-left: auto;\n }\n .gear-icon:hover { opacity: 1; }\n .utilization {\n font-size: 0.75em;\n font-weight: 600;\n }\n .utilization-normal { color: #4caf50; }\n .utilization-warning { color: #ff9800; }\n .utilization-alert { color: #f44336; }\n .circuit-alert {\n border-color: #f44336 !important;\n box-shadow: 0 0 8px rgba(244, 67, 54, 0.3);\n }\n .circuit-custom-monitoring {\n border-left: 3px solid #ff9800;\n }\n\n .chart-container {\n width: 100%;\n aspect-ratio: 4 / 1;\n margin-top: 4px;\n overflow: hidden;\n min-width: 0;\n }\n\n .sub-devices {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 12px;\n margin-bottom: 20px;\n padding-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .sub-device {\n background: var(--secondary-background-color, var(--card-background-color, #2a2a2a));\n border: 1px solid var(--divider-color, #333);\n border-radius: 12px;\n padding: 14px 16px;\n }\n .sub-device-bess,\n .sub-device-full {\n grid-column: 1 / -1;\n }\n\n .sub-device-header { display: flex; gap: 10px; align-items: baseline; margin-bottom: 8px; }\n .sub-device-type { font-size: 0.7em; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: var(--span-accent); }\n .sub-device-name { font-size: 0.85em; color: var(--secondary-text-color, #999); flex: 1; }\n .sub-power-value { font-size: 0.9em; color: var(--primary-text-color, #fff); white-space: nowrap; }\n .sub-power-value strong { font-weight: 700; font-size: 1.1em; }\n .sub-device .chart-container { margin-bottom: 8px; aspect-ratio: auto; }\n\n .bess-charts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(0, 1fr));\n gap: 12px;\n margin-bottom: 10px;\n }\n .bess-chart-col { min-width: 0; }\n .bess-chart-title {\n font-size: 0.75em;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n color: var(--secondary-text-color, #999);\n margin-bottom: 4px;\n }\n .bess-chart-col .chart-container { aspect-ratio: auto; }\n .sub-entity { display: flex; gap: 6px; padding: 3px 0; font-size: 0.85em; }\n .sub-entity-name { color: var(--secondary-text-color, #999); }\n .sub-entity-value { font-weight: 500; color: var(--primary-text-color, #e0e0e0); }\n\n /* ── Shared tab bar ────────────────────────────────────── */\n\n .shared-tab-bar {\n display: flex;\n gap: 0;\n margin-bottom: 16px;\n border-bottom: 1px solid var(--divider-color, #333);\n }\n\n .shared-tab {\n padding: 8px 16px;\n cursor: pointer;\n font-size: 0.9em;\n font-weight: 500;\n color: var(--primary-text-color);\n opacity: 0.6;\n border: none;\n border-bottom: 2px solid transparent;\n background: none;\n transition: opacity 0.15s;\n }\n\n .shared-tab:hover {\n opacity: 0.85;\n }\n\n .shared-tab.active {\n opacity: 1;\n border-bottom-color: var(--span-accent);\n }\n\n /* ── List view search ──────────────────────────────────── */\n\n .list-search-container {\n margin-bottom: 12px;\n position: relative;\n }\n\n .list-search {\n width: 100%;\n padding: 8px 36px 8px 12px;\n border-radius: 8px;\n border: 1px solid var(--divider-color, #333);\n background: var(--secondary-background-color, #2a2a2a);\n color: var(--primary-text-color);\n font-size: 0.9em;\n box-sizing: border-box;\n outline: none;\n }\n\n .list-search:focus {\n border-color: var(--span-accent);\n }\n\n .list-search-clear {\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 2px;\n display: flex;\n align-items: center;\n opacity: 0.7;\n }\n\n .list-search-clear:hover {\n opacity: 1;\n }\n\n .list-unit-toggle {\n display: inline-flex;\n margin-bottom: 12px;\n }\n\n /* ── List rows ─────────────────────────────────────────── */\n\n .list-view {\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n /* Each circuit is wrapped in a .list-cell so the row + its optional\n expanded chart stay together. In single-column flex mode the cell\n just stacks naturally. In multi-column grid mode the cell becomes\n one grid item, so the chart is always in the same column as its\n row. Area headers (rendered as siblings, not inside a cell) span\n all columns via their inline "grid-column: 1 / -1". */\n .list-cell {\n display: flex;\n flex-direction: column;\n min-width: 0;\n }\n .list-view[data-columns="2"],\n .list-view[data-columns="3"] {\n display: grid;\n grid-template-columns: repeat(var(--list-cols), minmax(0, 1fr));\n gap: 6px 8px;\n flex-direction: initial;\n }\n /* On narrow viewports a 2/3-column list would squeeze rows into an\n unreadable shape, so force stacking regardless of user preference. */\n @media (max-width: 599px) {\n .list-view[data-columns="2"],\n .list-view[data-columns="3"] {\n display: flex;\n flex-direction: column;\n }\n }\n\n .list-row {\n display: flex;\n align-items: center;\n padding: 12px 16px;\n gap: 10px;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-radius: 8px;\n cursor: pointer;\n transition: background 0.15s;\n }\n\n .list-row:hover {\n background: var(--secondary-background-color, #2a2a2a);\n }\n\n .list-row.circuit-off {\n opacity: 0.5;\n }\n\n .list-row.list-row-expanded {\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n border-bottom-color: transparent;\n }\n\n .list-circuit-name {\n flex: 1;\n color: var(--primary-text-color);\n font-size: 0.9em;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .list-status-badge {\n font-size: 0.75em;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 4px;\n flex-shrink: 0;\n }\n\n .list-status-on {\n color: #4dd9af;\n }\n\n .list-status-off {\n color: #f44336;\n }\n\n .list-power-value {\n font-size: 0.9em;\n font-weight: 600;\n min-width: 70px;\n text-align: right;\n flex-shrink: 0;\n }\n\n .list-expand-toggle {\n background: none;\n border: none;\n color: var(--secondary-text-color);\n cursor: pointer;\n padding: 4px;\n transition: transform 0.2s;\n display: flex;\n align-items: center;\n flex-shrink: 0;\n }\n\n .list-expand-toggle.expanded {\n transform: rotate(180deg);\n }\n\n .list-row .gear-icon {\n background: transparent;\n border: none;\n padding: 2px;\n cursor: pointer;\n color: #555;\n display: inline-flex;\n align-items: center;\n }\n .list-row .gear-icon:hover {\n color: var(--primary-text-color);\n }\n\n /* ── Expanded circuit content ──────────────────────────── */\n\n .list-expanded-content {\n padding: 0;\n background: var(--card-background-color, #1c1c1c);\n border: 1px solid var(--divider-color, #333);\n border-top: none;\n border-radius: 0 0 8px 8px;\n margin-top: -6px;\n margin-bottom: 2px;\n }\n\n .circuit-slot.circuit-chart-only {\n border: none;\n margin: 0;\n background: none;\n padding: 8px 12px;\n min-height: 0;\n }\n\n /* ── Area headers ──────────────────────────────────────── */\n\n .area-header {\n padding: 16px 12px 6px;\n font-weight: 600;\n font-size: 0.85em;\n color: var(--secondary-text-color);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n }\n\n /* ── No results ────────────────────────────────────────── */\n\n .list-no-results {\n padding: 24px;\n text-align: center;\n color: var(--secondary-text-color);\n }\n\n';class Jt{constructor(){this._ctrl=new Qt,this._container=null,this._onGearClick=null,this._onToggleClick=null,this._onSidePanelClosed=null,this._onGraphSettingsChanged=null}get hass(){return this._ctrl.hass}set hass(e){this._ctrl.hass=e}set errorStore(e){this._ctrl.errorStore=e}setPanelFavorites(e){this._ctrl.setPanelFavorites(e)}async render(e,t,i,s,o){let r,a;this.stop(),this._ctrl.reset(),this._ctrl.showMonitoring=!0,this._container=e,this._ctrl.hass=t;try{const e=await Ye(t,i);r=e.topology,a=e.panelSize}catch(t){return void(e.innerHTML=`${Oe(t.message)}
`)}this._ctrl.init(r,s,t,o??null),await this._ctrl.monitoringCache.fetch(t,o??null),await this._ctrl.fetchAndBuildHorizonMaps();const l=Math.ceil(a/2),c=this._ctrl.monitoringCache.status,d=nt(r,s),h=function(e){if(!e)return"";const t=Object.values(e.circuits??{}),i=Object.values(e.mains??{}),s=[...t,...i],o=s.filter(e=>void 0!==e.utilization_pct&&e.utilization_pct>=80&&e.utilization_pct<100).length,r=s.filter(e=>void 0!==e.utilization_pct&&e.utilization_pct>=100).length,a=s.filter(e=>e.has_override).length;return`\n \n ✓ ${n("status.monitoring")} · ${t.length} ${n("status.circuits")} · ${i.length} ${n("status.mains")} \n \n ${o>0?`${o} ${n(o>1?"status.warnings":"status.warning")} `:""}\n ${r>0?`${r} ${n(r>1?"status.alerts":"status.alert")} `:""}\n ${a>0?`${a} ${n(a>1?"status.overrides":"status.override")} `:""}\n \n
\n `}(c),p=function(e,t,n,i,s){const o=new Map,r=new Set;for(const[t,n]of Object.entries(e.circuits)){const e=n.tabs;if(!e||0===e.length)continue;const i=Math.min(...e),s=1===e.length?"single":ct(e)??"single";o.set(i,{uuid:t,circuit:n,layout:s});for(const t of e)r.add(t)}const a=new Set,l=new Set;for(const[e,t]of o)if("col-span"===t.layout){const n=t.circuit.tabs,i=at(Math.max(...n));0===lt(e)?a.add(i):l.add(i)}function c(e){const t=e.circuit.entities?.current??e.circuit.entities?.power,i=s?gt(s,t??""):null;let o;if(e.circuit.always_on)o="always_on";else{const t=e.circuit.entities?.select;o=t&&n.states[t]?n.states[t].state:"unknown"}return{monInfo:i,sheddingPriority:o}}let d="";for(let e=1;e<=t;e++){const t=2*e-1,s=2*e,h=o.get(t),p=o.get(s);if(d+=`${t}
`,h&&"row-span"===h.layout){const{monInfo:t,sheddingPriority:o}=c(h);d+=vt(h.uuid,h.circuit,e,"2 / 4","row-span",n,i,t,o),d+=`${s}
`;continue}if(!a.has(e))if(!h||"col-span"!==h.layout&&"single"!==h.layout)r.has(t)||(d+=mt(e,"2"));else{const{monInfo:t,sheddingPriority:s}=c(h);d+=vt(h.uuid,h.circuit,e,"2",h.layout,n,i,t,s)}if(!l.has(e))if(!p||"col-span"!==p.layout&&"single"!==p.layout)r.has(s)||(d+=mt(e,"3"));else{const{monInfo:t,sheddingPriority:s}=c(p);d+=vt(p.uuid,p.circuit,e,"3",p.layout,n,i,t,s)}d+=`${s}
`}return d}(r,l,t,s,c),u=Et(r,t,s);e.innerHTML=`\n \n ${d}\n ${h}\n ${u?`${u}
`:""}\n ${!1!==s.show_panel?`\n \n ${p}\n
\n `:""}\n \n `,this._onGearClick=t=>{this._ctrl.onGearClick(t,e)},this._onToggleClick=t=>{this._ctrl.onToggleClick(t,e)},e.addEventListener("click",this._onGearClick),e.addEventListener("click",this._onToggleClick),this._onSidePanelClosed=()=>{this._ctrl.monitoringCache.invalidate(),this._ctrl.graphSettingsCache.invalidate()},e.addEventListener("side-panel-closed",this._onSidePanelClosed),this._onGraphSettingsChanged=()=>this._ctrl.onGraphSettingsChanged(e),e.addEventListener("graph-settings-changed",this._onGraphSettingsChanged);try{await this._ctrl.loadHistory()}catch{}this._ctrl.updateDOM(e);const g=e.querySelector(".slide-confirm");g&&(this._ctrl.bindSlideConfirm(g,e),e.classList.add("switches-disabled")),this._ctrl.setupResizeObserver(e,e),this._ctrl.startIntervals(e)}stop(){this._ctrl.stopIntervals(),this._container&&(this._onGearClick&&(this._container.removeEventListener("click",this._onGearClick),this._onGearClick=null),this._onToggleClick&&(this._container.removeEventListener("click",this._onToggleClick),this._onToggleClick=null),this._onSidePanelClosed&&(this._container.removeEventListener("side-panel-closed",this._onSidePanelClosed),this._onSidePanelClosed=null),this._onGraphSettingsChanged&&(this._container.removeEventListener("graph-settings-changed",this._onGraphSettingsChanged),this._onGraphSettingsChanged=null))}}const Xt="\n display:flex;align-items:center;gap:8px;margin-bottom:8px;\n",Zt="\n background:var(--secondary-background-color,#333);\n border:1px solid var(--divider-color);\n color:var(--primary-text-color);\n border-radius:4px;padding:6px 10px;width:80px;font-size:0.85em;\n",Yt="\n min-width:130px;font-size:0.85em;color:var(--secondary-text-color);\n",en="\n min-width:160px;font-size:0.85em;color:var(--secondary-text-color);\n",tn="\n background:var(--secondary-background-color,#333);\n border:1px solid var(--divider-color);\n color:var(--primary-text-color);\n border-radius:4px;padding:6px 10px;flex:1;font-size:0.85em;\n font-family:monospace;\n";function nn(e,t,n,i,s){return`\n ${i} \n `}class sn{constructor(){this.errorStore=null,this._debounceTimer=null,this._configEntryId=null,this._notifyCloseHandler=null,this._headerHTML=""}stop(){this._notifyCloseHandler&&(document.removeEventListener("click",this._notifyCloseHandler),this._notifyCloseHandler=null),this._debounceTimer&&(clearTimeout(this._debounceTimer),this._debounceTimer=null)}async render(e,t,i,s=""){let o;void 0!==i&&(this._configEntryId=i),this._headerHTML=s,this._notifyCloseHandler&&(document.removeEventListener("click",this._notifyCloseHandler),this._notifyCloseHandler=null);try{const e={};this._configEntryId&&(e.config_entry_id=this._configEntryId);const n=await t.callWS({type:"call_service",domain:a,service:"get_monitoring_status",service_data:e,return_response:!0});o=function(e){if(!e||"object"!=typeof e)return null;const t=e,n={};return"boolean"==typeof t.enabled&&(n.enabled=t.enabled),t.global_settings&&"object"==typeof t.global_settings&&(n.global_settings=t.global_settings),t.circuits&&"object"==typeof t.circuits&&(n.circuits=t.circuits),t.mains&&"object"==typeof t.mains&&(n.mains=t.mains),n}(n?.response)}catch(e){console.warn("SPAN Panel: monitoring status fetch failed",e),o=null}const r=o?.global_settings??{},l=!0===o?.enabled,c=o?.circuits??{},d=o?.mains??{},h=new Set;for(const e of Object.keys(t.states||{}))e.startsWith("notify.")&&h.add(e);const p=new Set(["notify","send_message"]);for(const e of Object.keys(t.services?.notify||{}))p.has(e)||h.add(`notify.${e}`);h.add("event_bus");const u=[...h].sort(),g=r.notify_targets??"",_=("string"==typeof g?g.split(","):g).map(e=>e.trim()).filter(Boolean),f=u.length>0&&u.every(e=>_.includes(e)),v=r.notification_title_template??"SPAN: {name} {alert_type}",m=r.notification_message_template??"{name} at {current_a}A ({utilization_pct}% of {breaker_rating_a}A rating)",b=r.notification_priority??"default",y=Object.entries(c).sort(([,e],[,t])=>(e.name??"").localeCompare(t.name??"")),w=Object.entries(d),x=[...y,...w],S=x.length>0&&x.every(([,e])=>!1!==e.monitoring_enabled),$=x.some(([,e])=>!1!==e.monitoring_enabled),C=y.map(([e,t])=>{const i=Oe(t.name??e),s=!1!==t.monitoring_enabled,o=!0===t.has_override,r=s?"":"opacity:0.4;",a=Oe(e);return`\n \n \n \n \n ${i} \n \n \n ${nn(a,"continuous_threshold_pct",t.continuous_threshold_pct,"%","circuit")}\n ${nn(a,"spike_threshold_pct",t.spike_threshold_pct,"%","circuit")}\n ${nn(a,"window_duration_m",t.window_duration_m,"m","circuit")}\n ${nn(a,"cooldown_duration_m",t.cooldown_duration_m,"m","circuit")}\n \n ${o?`\n ${n("monitoring.reset")}\n `:""}\n \n \n `}).join(""),P=Object.entries(d).map(([e,t])=>{const i=Oe(t.name??e),s=!1!==t.monitoring_enabled,o=!0===t.has_override,r=s?"":"opacity:0.4;",a=Oe(e);return`\n \n \n \n \n ${i} \n \n \n ${nn(a,"continuous_threshold_pct",t.continuous_threshold_pct,"%","mains")}\n ${nn(a,"spike_threshold_pct",t.spike_threshold_pct,"%","mains")}\n ${nn(a,"window_duration_m",t.window_duration_m,"m","mains")}\n ${nn(a,"cooldown_duration_m",t.cooldown_duration_m,"m","mains")}\n \n ${o?`\n ${n("monitoring.reset")}\n `:""}\n \n \n `}).join("");e.innerHTML=`\n ${this._headerHTML}\n \n
${n("monitoring.heading")} \n\n
\n
\n
${n("monitoring.global_settings")} \n \n \n ${n("monitoring.enabled")} \n \n \n\n
\n
\n ${n("monitoring.continuous")} \n \n
\n
\n ${n("monitoring.spike")} \n \n
\n
\n ${n("monitoring.window")} \n \n
\n
\n ${n("monitoring.cooldown")} \n \n
\n\n
\n
${n("notification.heading")} \n\n
\n
${n("notification.targets")} \n
\n \n ${n("notification.all_targets")} \n \n
\n
\n ${_.length?_.map(e=>"event_bus"===e?n("notification.event_bus_target"):Oe(e)).join(", "):n("notification.none_selected")} \n ▼ \n \n
\n ${0===u.length?`
${n("notification.no_targets")}
`:u.map(e=>{const i=_.includes(e),s="event_bus"===e,o=s?null:t.states[e],r=o?.attributes?.friendly_name,a=s?n("notification.event_bus_target"):r?`${Oe(r)} (${Oe(e)})`:Oe(e);return`
\n \n ${a} \n `}).join("")}\n
\n
\n
\n\n
\n ${n("notification.priority")} \n \n ${["default","passive","active","time-sensitive","critical"].map(e=>`${n(`notification.priority.${e.replace("-","_")}`)} `).join("")}\n \n \n ${"critical"===b?n("notification.hint.critical"):"time-sensitive"===b?n("notification.hint.time_sensitive"):"passive"===b?n("notification.hint.passive"):"active"===b?n("notification.hint.active"):""}\n \n
\n\n
\n ${n("notification.title_template")} \n \n
\n\n
\n ${n("notification.message_template")} \n \n
\n\n
\n ${n("notification.placeholders")} {name} {entity_id} {alert_type}\n {current_a} {breaker_rating_a} {threshold_pct}\n {utilization_pct} {window_m} {local_time}\n
\n
\n ${n("notification.event_bus_help")} span_panel_current_alert\n ${n("notification.event_bus_payload")} alert_source alert_id\n alert_name alert_type current_a\n breaker_rating_a threshold_pct utilization_pct\n panel_serial window_duration_s local_time\n
\n\n
\n ${n("notification.test_label")} \n \n ${n("notification.test_button")}\n \n \n
\n
\n\n
\n
\n\n
${n("monitoring.monitored_points")} \n
\n
\n `;const k=e.querySelector("#toggle-all-circuits");k&&!S&&$&&(k.indeterminate=!0);const E=e.querySelector("#notify-all-targets");if(E&&u.length>0){const e=_.length>0;!f&&e&&(E.indeterminate=!0)}this._bindGlobalControls(e,t),this._bindNotifyTargetSelect(e,t),this._bindNotificationSettings(e,t),this._bindToggleAll(e,t,c,d),this._bindCircuitToggles(e,t),this._bindMainsToggles(e,t),this._bindThresholdInputs(e,t),this._bindResetButtons(e,t)}_serviceData(e){return this._configEntryId&&(e.config_entry_id=this._configEntryId),e}_callSetGlobal(e,t){return e.callWS({type:"call_service",domain:a,service:"set_global_monitoring",service_data:this._serviceData({...t})})}_bindGlobalControls(e,t){const i=e.querySelector("#monitoring-enabled"),s=e.querySelector("#global-fields"),o=e.querySelector("#global-status"),r=()=>{const t=[["continuous_threshold_pct","#g-continuous"],["spike_threshold_pct","#g-spike"],["window_duration_m","#g-window"],["cooldown_duration_m","#g-cooldown"]],n={};for(const[i,s]of t){const t=e.querySelector(s);if(!t)return null;const o=parseInt(t.value,10);if(Number.isNaN(o))return null;n[i]=o}return n},a=(e,t,i)=>{if(!e)return;const s=t instanceof Error?t.message:i;e.textContent=`${n("error.prefix")} ${s}`,e.style.color="var(--error-color, #f44336)"},l=()=>{this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{const i=r();if(i)try{await this._callSetGlobal(t,i),await this.render(e,t)}catch(e){a(o,e,n("error.failed_save"))}else a(o,null,n("error.failed_save"))},u)};i&&i.addEventListener("change",async()=>{const o=i.checked;s&&(s.style.opacity=o?"":"0.4",s.style.pointerEvents=o?"":"none");const l=e.querySelector("#global-status");try{if(o){const e=r();if(!e)return void a(l,null,n("error.failed"));await this._callSetGlobal(t,e)}else await this._callSetGlobal(t,{enabled:!1})}catch(e){return void a(l,e,n("error.failed"))}await this.render(e,t)});for(const t of e.querySelectorAll("#global-fields input[type=number]"))t.addEventListener("input",l)}_bindNotifyTargetSelect(e,t){const i=e.querySelector("#notify-target-btn"),s=e.querySelector("#notify-target-dropdown"),o=e.querySelector("#notify-target-label");if(!i||!s)return;i.addEventListener("click",e=>{e.stopPropagation();const t="none"!==s.style.display;s.style.display=t?"none":"block"});const r=t=>{const n=e.querySelector("#notify-target-select");n&&!n.contains(t.target)&&(s.style.display="none")};document.addEventListener("click",r),this._notifyCloseHandler=r;const a=()=>{const i=[...e.querySelectorAll(".notify-target-cb:checked")].map(e=>e.value);if(o){const e=i.map(e=>"event_bus"===e?n("notification.event_bus_target"):e);o.textContent=e.length?e.join(", "):n("notification.none_selected")}const s=e.querySelector("#notify-all-targets");if(s){const t=[...e.querySelectorAll(".notify-target-cb")];s.checked=t.length>0&&t.every(e=>e.checked),s.indeterminate=!s.checked&&t.some(e=>e.checked)}this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{try{await this._callSetGlobal(t,{notify_targets:i.join(", ")})}catch(e){console.warn("SPAN Panel: notification targets save failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})}},u)},l=e.querySelector("#notify-all-targets");l&&l.addEventListener("change",()=>{for(const t of e.querySelectorAll(".notify-target-cb"))t.checked=l.checked;const t=e.querySelector("#notify-target-btn");t&&(t.style.opacity=l.checked?"0.4":"",t.style.pointerEvents=l.checked?"none":""),l.checked&&(s.style.display="none"),a()});for(const t of e.querySelectorAll(".notify-target-cb"))t.addEventListener("change",()=>{a()})}_bindNotificationSettings(e,t){const i=e.querySelector("#g-priority"),s=e.querySelector("#g-title-template"),o=e.querySelector("#g-message-template"),r=(e,i)=>{this._debounceTimer&&clearTimeout(this._debounceTimer),this._debounceTimer=setTimeout(async()=>{try{await this._callSetGlobal(t,{[e]:i})}catch(e){console.warn("SPAN Panel: notification settings save failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})}},u)};i&&i.addEventListener("change",async()=>{try{await this._callSetGlobal(t,{notification_priority:i.value}),await this.render(e,t)}catch(e){console.warn("SPAN Panel: notification priority change failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})}}),s&&s.addEventListener("input",()=>{r("notification_title_template",s.value)}),o&&o.addEventListener("input",()=>{r("notification_message_template",o.value)});const l=e.querySelector("#test-notification-btn"),c=e.querySelector("#test-notification-status");l&&l.addEventListener("click",async()=>{l.disabled=!0,c&&(c.textContent=n("notification.test_sending"),c.style.color="var(--secondary-text-color)");try{this._debounceTimer&&(clearTimeout(this._debounceTimer),this._debounceTimer=null);const i=[...e.querySelectorAll(".notify-target-cb:checked")].map(e=>e.value).join(", ");await this._callSetGlobal(t,{notify_targets:i});const s={};this._configEntryId&&(s.config_entry_id=this._configEntryId),await t.callWS({type:"call_service",domain:a,service:"test_notification",service_data:s}),c&&(c.textContent=n("notification.test_sent"),c.style.color="var(--success-color, #4caf50)")}catch(e){if(c){const t=e instanceof Error?e.message:n("error.failed");c.textContent=`${n("error.prefix")} ${t}`,c.style.color="var(--error-color, #f44336)"}}finally{l.disabled=!1}})}_bindToggleAll(e,t,i,s){const o=e.querySelector("#toggle-all-circuits");o&&o.addEventListener("change",async()=>{const r=o.checked,l=[...Object.keys(i).map(e=>t.callWS({type:"call_service",domain:a,service:"set_circuit_threshold",service_data:this._serviceData({circuit_id:e,monitoring_enabled:r})}).catch(e=>{console.warn("SPAN Panel: circuit monitoring toggle failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})})),...Object.keys(s).map(e=>t.callWS({type:"call_service",domain:a,service:"set_mains_threshold",service_data:this._serviceData({leg:e,monitoring_enabled:r})}).catch(e=>{console.warn("SPAN Panel: mains monitoring toggle failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1})}))];await Promise.all(l),await this.render(e,t)})}_bindMainsToggles(e,t){for(const i of e.querySelectorAll(".mains-toggle"))i.addEventListener("change",async()=>{const s=i.dataset.entity,o=i.checked;try{await t.callWS({type:"call_service",domain:a,service:"set_mains_threshold",service_data:this._serviceData({leg:s,monitoring_enabled:o})})}catch(e){return console.warn("SPAN Panel: mains threshold toggle failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1}),void(i.checked=!o)}await this.render(e,t)})}_bindCircuitToggles(e,t){for(const i of e.querySelectorAll(".circuit-toggle"))i.addEventListener("change",async()=>{const s=i.dataset.entity,o=i.checked;try{await t.callWS({type:"call_service",domain:a,service:"set_circuit_threshold",service_data:this._serviceData({circuit_id:s,monitoring_enabled:o})})}catch(e){return console.warn("SPAN Panel: circuit threshold toggle failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1}),void(i.checked=!o)}await this.render(e,t)})}_bindThresholdInputs(e,t){const i=new Map;for(const s of e.querySelectorAll(".threshold-input"))s.addEventListener("input",()=>{const o=`${s.dataset.entity}-${s.dataset.field}`,r=i.get(o);r&&clearTimeout(r),i.set(o,setTimeout(async()=>{const i=parseInt(s.value,10);if(!i||i<1)return;const o=s.dataset.entity,r=s.dataset.field,l=s.dataset.type,c="mains"===l?"set_mains_threshold":"set_circuit_threshold",d="mains"===l?"leg":"circuit_id";try{await t.callWS({type:"call_service",domain:a,service:c,service_data:this._serviceData({[d]:o,[r]:i})}),await this.render(e,t)}catch(e){console.warn("SPAN Panel: threshold input save failed",e),this.errorStore?.add({key:"service:monitoring",level:"error",message:n("error.threshold_failed"),persistent:!1}),s.style.borderColor="var(--error-color, #f44336)"}},800))})}_bindResetButtons(e,t){for(const n of e.querySelectorAll(".reset-btn"))n.addEventListener("click",async()=>{const i=n.dataset.entity;if(!i)return;const s=n.dataset.type,o="mains"===s?"clear_mains_threshold":"clear_circuit_threshold",r=this._serviceData("mains"===s?{leg:i}:{circuit_id:i});await t.callService(a,o,r),await this.render(e,t)})}}function on(e=""){const t=e?` value="${Oe(e)}"`:"",i=e?"":"display:none;";return`\n \n \n \n \n \n
\n `}function rn(e,t,i,s,o,r,a){const c=t.entities?.power,d=c?i.states[c]:null,h=d&&parseFloat(d.state)||0,p=t.entities?.switch,u=p?i.states[p]:null,g=u?"on"===u.state:(d?.attributes?.relay_state||t.relay_state)===l,_=t.breaker_rating_a,m=_?`${Math.round(_)}A`:"",b=Oe(t.name||n("grid.unknown")),y=dt(s),w="current"===y.entityRole;let x;if(g)if(w){const e=t.entities?.current,n=e?i.states[e]:null,s=n&&parseFloat(n.state)||0;x=`${y.format(s)} A `}else x=`${ot(h)} ${st(h)} `;else x="";const S=r||"unknown";let $="";if("unknown"!==S){const e=f[S]??f.unknown??{icon:"mdi:help",color:"#999",label:()=>"Unknown"};$=e.icon2?`\n \n \n `:e.textLabel?`\n \n ${e.textLabel} \n `:` `}let C="",P=o?.utilization_pct??null;if(null==P&&t.breaker_rating_a){const e=t.entities?.current,n=e?i.states[e]:null,s=n?Math.abs(parseFloat(n.state)||0):0;P=Math.round(s/t.breaker_rating_a*1e3)/10}if(null!=P){C=`=80?"utilization-warning":"utilization-normal"}">${Math.round(P)}% `}const k=!!o&&_t(o)?v:"#555",E=`\n \n `,z=!1!==t.is_user_controllable&&!!t.entities?.switch?`\n ${n(g?"grid.on":"grid.off")} \n \n
`:`${g?"ON":"OFF"} `;return`\n \n ${m?`${m} `:""}\n ${C}\n ${b} \n ${$}\n ${z}\n \n ${x}\n \n ${E}\n \n \n \n
\n `}function an(e,t,n,i,s){const o=t.entities?.power,r=o?n.states[o]:null,a=r&&parseFloat(r.state)||0,d=t.device_type===c||a<0,h=t.entities?.switch,p=h?n.states[h]:null,u=ft(0,s,p?"on"===p.state:(r?.attributes?.relay_state||t.relay_state)===l,d),g=Oe(e);return`\n \n `}function ln(e){return``}function cn(e,t,n){const i=e.entities?.switch,s=i?t.states[i]:null,o=e.entities?.power,r=o?t.states[o]:null,a=s?"on"===s.state:(r?.attributes?.relay_state||e.relay_state)===l;let c;if("current"===(n.chart_metric||"power")){const n=e.entities?.current,i=n?t.states[n]:null;c=i?Math.abs(parseFloat(i.state)||0):0}else c=r?Math.abs(parseFloat(r.state)||0):0;return{isOn:a,value:c}}function dn(e,t){if(e.always_on)return"always_on";const n=e.entities?.select,i=n?t.states[n]:null;return i?i.state:"unknown"}function hn(e,t,n,i){const s=cn(e,n,i),o=cn(t,n,i);return s.isOn&&!o.isOn?-1:!s.isOn&&o.isOn?1:o.value-s.value}function pn(e,t,n){return e.sort((e,i)=>hn(e[1],i[1],t,n))}function un(e){return e.entities?.current??e.entities?.power??""}class gn{constructor(e){this._expandedUuids=new Set,this._searchQuery="",this._container=null,this._clickHandler=null,this._inputHandler=null,this._graphSettingsHandler=null,this._hass=null,this._topology=null,this._config=null,this._monitoringStatus=null,this._viewName=null,this._columns=1,this._ctrl=e}setColumns(e){const t=Math.max(1,Math.min(3,Math.floor(e)));this._columns=t}setInitialExpansion(e){this._expandedUuids=new Set(e)}setInitialSearchQuery(e){this._searchQuery=e}setViewName(e){this._viewName=e}renderActivityView(e,t,n,i,s,o){this._unbindEvents(),this._hass=t,this._topology=n,this._config=i,this._monitoringStatus=s;const r=pn(Object.entries(n.circuits),t,i);let a=o+on(this._searchQuery);a+=``;for(const[e,n]of r){const o=gt(s,un(n)),r=dn(n,t),l=this._expandedUuids.has(e);a+=`
`,a+=rn(e,n,t,i,o,r,l),l&&(a+=an(e,n,t,0,o)),a+="
"}a+="
",a+=" ",e.innerHTML=a;const l=e.querySelector("span-side-panel");l&&(l.hass=t,l.errorStore=this._ctrl.errorStore),this._bindEvents(e),this._searchQuery&&this._applyFilter(e),this._ctrl.updateDOM(e)}renderAreaView(e,t,i,s,o,r){this._unbindEvents(),this._hass=t,this._topology=i,this._config=s,this._monitoringStatus=o;const a=n("list.unassigned_area"),l=new Map;for(const[e,t]of Object.entries(i.circuits)){const n=t.area??a,i=l.get(n);i?i.push([e,t]):l.set(n,[[e,t]])}const c=[...l.keys()].sort((e,t)=>e===a?1:t===a?-1:e.localeCompare(t));let d=r+on(this._searchQuery);d+=``;for(const e of c){const n=l.get(e);if(!n)continue;const i=pn(n,t,s);d+=ln(e);for(const[e,n]of i){const i=gt(o,un(n)),r=dn(n,t),a=this._expandedUuids.has(e);d+=`
`,d+=rn(e,n,t,s,i,r,a),a&&(d+=an(e,n,t,0,i)),d+="
"}}d+="
",d+=" ",e.innerHTML=d;const h=e.querySelector("span-side-panel");h&&(h.hass=t,h.errorStore=this._ctrl.errorStore),this._bindEvents(e),this._searchQuery&&this._applyFilter(e),this._ctrl.updateDOM(e)}updateCollapsedRows(e,t,i,s){const o=dt(s),r="current"===o.entityRole,a=e.querySelectorAll(".list-row[data-row-uuid]");for(const e of a){const a=e.dataset.rowUuid;if(!a)continue;const l=i.circuits[a];if(!l)continue;const{isOn:c,value:d}=cn(l,t,s),h=e.querySelector(".list-power-value");if(h)if(c)if(r)h.innerHTML=`${o.format(d)} A `;else{const e=l.entities?.power,n=e?t.states[e]:null,i=n&&parseFloat(n.state)||0;h.innerHTML=`${ot(i)} ${st(i)} `}else h.innerHTML="";const p=e.querySelector(".toggle-pill");if(p){p.classList.toggle("toggle-on",c),p.classList.toggle("toggle-off",!c);const e=p.querySelector(".toggle-label");e&&(e.textContent=n(c?"grid.on":"grid.off"))}const u=e.querySelector(".list-status-badge");u&&(u.textContent=c?"ON":"OFF",u.classList.toggle("list-status-on",c),u.classList.toggle("list-status-off",!c)),e.classList.toggle("circuit-off",!c)}!function(e,t,n,i){const s=e.querySelector(".list-view");if(s)for(const e of function(e,t){let n={anchor:null,units:[]};const i=[n];for(const s of[...e.children])if(s.classList.contains("area-header"))n={anchor:s,units:[]},i.push(n);else if(s.classList.contains("list-cell")){const e=s.dataset.cellUuid,i=e?t.circuits[e]:void 0;e&&i&&n.units.push({cell:s,uuid:e,circuit:i})}return i}(s,n)){if(e.units.length<2)continue;const n=[...e.units].sort((e,n)=>hn(e.circuit,n.circuit,t,i));if(!n.some((t,n)=>t.uuid!==e.units[n].uuid))continue;let o=e.anchor;for(const e of n)o?o.after(e.cell):s.prepend(e.cell),o=e.cell}}(e,t,i,s)}stop(){this._unbindEvents(),null===this._viewName&&(this._expandedUuids.clear(),this._searchQuery=""),this._hass=null,this._topology=null,this._config=null,this._monitoringStatus=null}_dispatchFavoritesViewState(){if(!this._viewName||!this._container)return;const e={view:this._viewName,expanded:[...this._expandedUuids],searchQuery:this._searchQuery};this._container.dispatchEvent(new CustomEvent("favorites-view-state-changed",{detail:e,bubbles:!0,composed:!0}))}_bindEvents(e){this._container=e,this._clickHandler=t=>{const n=t.target;if(!n)return;const i=n.closest(".list-expand-toggle");if(i){const e=i.dataset.expandUuid;return void(e&&this._toggleExpand(e))}if(n.closest(".gear-icon"))return void this._ctrl.onGearClick(t,e);if(n.closest(".toggle-pill"))return void this._ctrl.onToggleClick(t,e);if(n.closest(".list-search-clear")){const t=e.querySelector(".list-search");return void(t&&(t.value="",t.dispatchEvent(new Event("input",{bubbles:!0}))))}const s=n.closest(".unit-btn");if(s){const t=s.dataset.unit;t&&e.dispatchEvent(new CustomEvent("unit-changed",{detail:t,bubbles:!0,composed:!0}))}},this._inputHandler=t=>{const n=t.target;n&&n.classList.contains("list-search")&&(this._searchQuery=n.value.toLowerCase(),this._applyFilter(e),this._dispatchFavoritesViewState())},this._graphSettingsHandler=()=>{this._ctrl.onGraphSettingsChanged(e).then(()=>{this._ctrl.updateDOM(e)}).catch(()=>{})},e.addEventListener("click",this._clickHandler),e.addEventListener("input",this._inputHandler),e.addEventListener("graph-settings-changed",this._graphSettingsHandler);const t=e.querySelector(".slide-confirm");t&&(this._ctrl.bindSlideConfirm(t,e),e.classList.add("switches-disabled"))}_unbindEvents(){this._container&&(this._clickHandler&&this._container.removeEventListener("click",this._clickHandler),this._inputHandler&&this._container.removeEventListener("input",this._inputHandler),this._graphSettingsHandler&&this._container.removeEventListener("graph-settings-changed",this._graphSettingsHandler)),this._container=null,this._clickHandler=null,this._inputHandler=null,this._graphSettingsHandler=null}_applyFilter(e){const t=e.querySelector(".list-search-clear");t&&(t.style.display=this._searchQuery?"":"none");const n=e.querySelectorAll(".list-cell[data-cell-uuid]");for(const e of n){const t=e.querySelector(".list-circuit-name"),n=(t?.textContent?.toLowerCase()??"").includes(this._searchQuery);e.style.display=n?"":"none"}const i=e.querySelectorAll(".area-header");for(const e of i){let t=!1,n=e.nextElementSibling;for(;n&&!n.classList.contains("area-header");){if(n.classList.contains("list-cell")&&"none"!==n.style.display){t=!0;break}n=n.nextElementSibling}e.style.display=t?"":"none"}}_toggleExpand(e){if(!(this._container&&this._hass&&this._topology&&this._config))return;const t=jt(e),n=this._container.querySelector(`.list-cell[data-cell-uuid="${t}"]`);if(!n)return;const i=n.querySelector(`.list-row[data-row-uuid="${t}"]`),s=n.querySelector(`.list-expand-toggle[data-expand-uuid="${t}"]`);if(i){if(this._expandedUuids.has(e)){this._expandedUuids.delete(e);const o=n.querySelector(`.list-expanded-content[data-expanded-uuid="${t}"]`);o&&o.remove(),s&&s.classList.remove("expanded"),i.classList.remove("list-row-expanded")}else{this._expandedUuids.add(e);const t=this._topology.circuits[e];if(!t)return;const n=gt(this._monitoringStatus,un(t)),o=an(e,t,this._hass,this._config,n);i.insertAdjacentHTML("afterend",o),s&&s.classList.add("expanded"),i.classList.add("list-row-expanded"),this._ctrl.updateDOM(this._container)}this._dispatchFavoritesViewState()}}}function _n(e,t){return`${e}|${t}`}class fn{async build(e,t,n,i){const s=new Map;for(const e of n)s.set(e.id,e);const o=i?new We(i):null,r=[];for(const[n,i]of Object.entries(t)){if(!((i?.circuits?.length??0)>0||(i?.sub_devices?.length??0)>0))continue;const t=s.get(n);t&&r.push((async()=>{try{const i=await Ye(e,n,o);return{panelDeviceId:n,panel:t,topology:i.topology}}catch(e){return console.warn("SPAN Panel: favorites topology fetch failed",n,e),{panelDeviceId:n,panel:t,topology:null}}})())}const a=(await Promise.all(r)).filter(e=>null!==e.topology),l=a.length>1,c={},d={},h={},p=new Set,u=[];for(const{panelDeviceId:e,panel:n,topology:i}of a){if(!i)continue;const s=n.config_entries?.[0]??null;s&&p.add(s);const o=n.name_by_user??n.name??i.device_name??"";u.push({panelDeviceId:e,panelName:o,topology:i});const r=t[e],a=r?.circuits??[],g=r?.sub_devices??[];for(const t of a){const n=i.circuits?.[t];if(!n)continue;const r=_n(e,t),a=l&&o?`${o} · ${n.name}`:n.name;c[r]={...n,name:a},h[r]={panelDeviceId:e,kind:"circuit",targetId:t,configEntryId:s}}for(const t of g){const n=i.sub_devices?.[t];if(!n)continue;const r=_n(e,t),a=l&&o&&n.name?`${o} · ${n.name}`:n.name??t;d[r]={...n,name:a},h[r]={panelDeviceId:e,kind:"sub_device",targetId:t,configEntryId:s}}}return{topology:{circuits:c,sub_devices:d,panel_entities:{},device_name:"",_favoriteRefs:h},entryIds:Array.from(p),perPanelStats:u}}}const vn="span_panel_favorites_view_state";function mn(e){try{localStorage.setItem(vn,JSON.stringify(e))}catch{}}var bn;const yn="favorites";let wn=bn=class extends Pe{constructor(){super(...arguments),this.narrow=!1,this._panels=[],this._selectedPanelId=null,this._activeTab="dashboard",this._discovered=!1,this._listColumns=qe(),this._favorites={},this._favoritesViewState={expanded:{activity:[],area:[]}},this._favoritesPanelStats=[],this._dashboardTab=new Jt,this._monitoringTab=new sn,this._listDashCtrl=new Qt,this._listCtrl=new gn(this._listDashCtrl),this._favCache=new Be,this._favCtrl=new fn,this._favoritesMonitoringTabs=new Map,this._errorStore=new Le,this._watchedPanelId=null,this._discovering=!1,this._refreshSeq=0,this._areaUnsub=null,this._areaSubscribing=!1,this._onVisibilityChange=null,this._onFavoritesChanged=null,this._deviceRegistryUnsub=null,this._pendingTabRender=!1,this._persistFavoritesViewStateTimer=null,this._tabRenderScheduler=function(e){let t=null,n=!1;return async function i(){if(t)return n=!0,void await t.catch(()=>{});const s=(async()=>{try{await e()}finally{t=null,n&&(n=!1,await i())}})();t=s,await s}}(async()=>this._renderTab()),this._beginRender=function(){let e=0;return()=>{e+=1;const t=e;return()=>e!==t}}()}get _root(){const e=this.shadowRoot;if(!e)throw new Error("span-panel: shadow root is not available");return e}connectedCallback(){super.connectedCallback(),this._dashboardTab.errorStore=this._errorStore,this._listDashCtrl.errorStore=this._errorStore,this._favCache.errorStore=this._errorStore,this._monitoringTab.errorStore=this._errorStore,this._onVisibilityChange=()=>{"visible"===document.visibilityState&&this._discovered&&this.hass&&this._scheduleTabRender()},document.addEventListener("visibilitychange",this._onVisibilityChange),this._onFavoritesChanged=()=>{this._refreshFavorites()},document.addEventListener(Ge,this._onFavoritesChanged),this._subscribeDeviceRegistry()}disconnectedCallback(){this._dashboardTab.stop(),this._monitoringTab.stop(),this._listCtrl.stop(),this._listDashCtrl.stopIntervals();for(const e of this._favoritesMonitoringTabs.values())e.stop();this._favoritesMonitoringTabs.clear(),this._areaSubscribing=!1,this._areaUnsub&&(this._areaUnsub(),this._areaUnsub=null),this._onVisibilityChange&&(document.removeEventListener("visibilitychange",this._onVisibilityChange),this._onVisibilityChange=null),this._onFavoritesChanged&&(document.removeEventListener(Ge,this._onFavoritesChanged),this._onFavoritesChanged=null),this._unsubscribeDeviceRegistry(),this._persistFavoritesViewStateTimer&&(clearTimeout(this._persistFavoritesViewStateTimer),this._persistFavoritesViewStateTimer=null),this._errorStore.dispose(),super.disconnectedCallback()}firstUpdated(){this.hass&&!this._discovered&&this._discoverPanels()}updated(e){if(e.has("hass")){const t=e.get("hass");this._dashboardTab.hass=this.hass,this._listDashCtrl.hass=this.hass,this._errorStore.updateHass(this.hass),this._discovered?this._root.getElementById("tab-content")||this._scheduleTabRender():this._discoverPanels(),!t&&this.hass&&this._subscribeDeviceRegistry()}if(this._discovered&&(e.has("_discovered")||e.has("_activeTab")||e.has("_selectedPanelId")||e.has("_chartMetric")||e.has("_listColumns"))){if(this._isFavoritesView&&"dashboard"===this._activeTab)return void(this._activeTab="activity");this._scheduleTabRender()}if(e.has("_selectedPanelId")&&(this._selectedPanelId!==yn&&this._selectedPanelId?(this._updatePanelStatusWatch(),this._listDashCtrl.setFavoritesPerPanelInfo(null)):(this._errorStore.clearPanelStatusWatch(),this._watchedPanelId=null)),this._discovered&&(e.has("_panels")||e.has("_selectedPanelId"))){const e=this.shadowRoot?.getElementById("panel-select");e&&null!==this._selectedPanelId&&e.value!==this._selectedPanelId&&(e.value=this._selectedPanelId)}if(e.has("hass")&&this._discovered&&("activity"===this._activeTab||"area"===this._activeTab)){const e=this._root.getElementById("tab-content"),t=this._listDashCtrl.topology;if(e&&t){this._listCtrl.updateCollapsedRows(e,this.hass,t,this._buildDashboardConfig());const n=e.querySelector("span-side-panel");n&&(n.hass=this.hass,n.errorStore=this._errorStore)}}}setConfig(e){}render(){var i,s,o;if(i=this.hass?.language,e=i&&t[i]?i:"en",!this.hass)return le`
+
+
+
${n("card.connecting")}
+
+ `;if(!this._discovered){const e=this._errorStore.hasPersistent("discovery-failed");return le`
+
+
+
+ ${e?de:le`
${n("card.connecting")}
`}
+
+ `}return le`
- `:se`
-
-
-
${this._discoveryError??"Loading…"}
-
- `}_onPanelChange(e){const t=e.target;this._selectedPanelId=t.value,localStorage.setItem("span_panel_selected",t.value),this._areaUnsub&&(this._areaUnsub(),this._areaUnsub=null),this._scheduleTabRender()}_onTabClick(e){const t=e.target.closest(".shared-tab");if(!t)return;const n=t.dataset.tab;n&&n!==this._activeTab&&(this._activeTab=n,this._scheduleTabRender())}_onTabContentClick(e){const t=e.target.closest(".unit-btn");if(t){const e=t.dataset.unit;if(!e||e===this._chartMetric)return;return this._chartMetric=e,localStorage.setItem("span_panel_metric",e),void("dashboard"===this._activeTab&&this._scheduleTabRender())}}_onSidePanelClosed(){if("dashboard"===this._activeTab){const e=this._dashboardTab._ctrl;e.monitoringCache.invalidate(),e.graphSettingsCache.invalidate()}}_onUnitChanged(e){const t=e.detail;t&&t!==this._chartMetric&&(this._chartMetric=t,localStorage.setItem("span_panel_metric",t),this._scheduleTabRender())}_onGraphSettingsChanged(){if("dashboard"===this._activeTab){const e=this.shadowRoot.getElementById("tab-content");if(e){this._dashboardTab._ctrl.onGraphSettingsChanged(e)}}}_onNavigateTab(e){const t=e.detail;t&&(this._activeTab=t,this._scheduleTabRender())}_subscribeDeviceRegistry(){!this._deviceRegistryUnsub&&this.hass?.connection&&(this._deviceRegistryUnsub=this.hass.connection.subscribeEvents(()=>this._refreshPanels(),"device_registry_updated"))}_unsubscribeDeviceRegistry(){this._deviceRegistryUnsub&&(this._deviceRegistryUnsub.then(e=>e()),this._deviceRegistryUnsub=null)}async _refreshPanels(){if(!this.hass||!this._discovered)return;const e=(await this.hass.callWS({type:"config/device_registry/list"})).filter(e=>e.identifiers?.some(e=>e[0]===a)&&!e.via_device_id),t=new Set(this._panels.map(e=>e.id)),n=new Set(e.map(e=>e.id));t.size===n.size&&[...t].every(e=>n.has(e))||(this._panels=e,!this._panels.some(e=>e.id===this._selectedPanelId)&&this._panels.length>0&&(this._selectedPanelId=this._panels[0].id,localStorage.setItem("span_panel_selected",this._selectedPanelId)))}async _discoverPanels(){if(!this.hass)return;try{const e=await this.hass.callWS({type:"config/device_registry/list"});this._panels=e.filter(e=>e.identifiers?.some(e=>e[0]===a)&&!e.via_device_id)}catch(e){return console.error("SPAN Panel: device discovery failed",e),void(this._discoveryError=`Discovery failed: ${e.message??e}`)}this._discoveryError=null,this._discovered=!0;const e=localStorage.getItem("span_panel_selected");e&&this._panels.some(t=>t.id===e)?this._selectedPanelId=e:this._panels.length>0&&(this._selectedPanelId=this._panels[0].id),this._chartMetric=localStorage.getItem("span_panel_metric")||"power"}_buildDashboardConfig(){return{chart_metric:this._chartMetric,history_minutes:5,show_panel:!0,show_battery:!0,show_evse:!0}}async _scheduleTabRender(){await this.updateComplete,await this._renderTab()}async _renderTab(){this._dashboardTab.stop(),this._monitoringTab.stop(),this._listCtrl.stop(),this._listDashCtrl.stopIntervals();const e=this.shadowRoot.getElementById("tab-content");if(e)switch(this._activeTab){case"dashboard":{e.innerHTML="";const t=this._buildDashboardConfig(),n=this._panels.find(e=>e.id===this._selectedPanelId),i=n?.config_entries?.[0]??null;await this._dashboardTab.render(e,this.hass,this._selectedPanelId??"",t,i);break}case"activity":{e.innerHTML="";const t=this._panels.find(e=>e.id===this._selectedPanelId),n=t?.config_entries?.[0]??null;try{const t=await Oe(this.hass,this._selectedPanelId??void 0),i=this._buildDashboardConfig();this._listDashCtrl.init(t.topology,i,this.hass,n),await this._listDashCtrl.monitoringCache.fetch(this.hass,n),await this._listDashCtrl.fetchAndBuildHorizonMaps(),this._listCtrl.renderActivityView(e,this.hass,t.topology,i,this._listDashCtrl.monitoringCache.status),e.insertAdjacentHTML("afterbegin",``),await this._listDashCtrl.loadHistory(),this._listDashCtrl.updateDOM(e),this._listDashCtrl.startIntervals(e)}catch(t){const n=document.createElement("p");n.style.color="var(--error-color)",n.textContent=t.message,e.appendChild(n)}break}case"area":{e.innerHTML="";const t=this._panels.find(e=>e.id===this._selectedPanelId),n=t?.config_entries?.[0]??null;try{const t=await Oe(this.hass,this._selectedPanelId??void 0),i=this._buildDashboardConfig();this._listDashCtrl.init(t.topology,i,this.hass,n),await this._listDashCtrl.monitoringCache.fetch(this.hass,n),await this._listDashCtrl.fetchAndBuildHorizonMaps(),this._listCtrl.renderAreaView(e,this.hass,t.topology,i,this._listDashCtrl.monitoringCache.status),e.insertAdjacentHTML("afterbegin",``),await this._listDashCtrl.loadHistory(),this._listDashCtrl.updateDOM(e),this._listDashCtrl.startIntervals(e),this._areaUnsub||async function(e,t,n){if(!e.connection)return()=>{};const i=async()=>{try{const i=new Map;for(const[e,n]of Object.entries(t.circuits))i.set(e,n.area);await Ie(e,t);for(const[e,o]of Object.entries(t.circuits))if(o.area!==i.get(e))return void n()}catch(e){console.warn("[span-panel] area registry update failed:",e)}},[o,s]=await Promise.all([e.connection.subscribeEvents(i,"entity_registry_updated"),e.connection.subscribeEvents(i,"area_registry_updated")]);return()=>{o(),s()}}(this.hass,t.topology,()=>{"area"===this._activeTab&&this._scheduleTabRender()}).then(e=>{this._areaUnsub=e}).catch(()=>{})}catch(t){const n=document.createElement("p");n.style.color="var(--error-color)",n.textContent=t.message,e.appendChild(n)}break}case"monitoring":{e.innerHTML="";const t=this._panels.find(e=>e.id===this._selectedPanelId),n=t?.config_entries?.[0]??null;await this._monitoringTab.render(e,this.hass,n??void 0);break}}}};Wt.styles=((e,...t)=>{const n=1===e.length?e[0]:t.reduce((t,n,i)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(n)+e[i+1],e[0]);return new x(n,e,y)})`
+ `}_onPanelChange(e){const t=e.target;this._selectedPanelId=t.value,localStorage.setItem("span_panel_selected",t.value),this._isFavoritesView&&"dashboard"===this._activeTab&&(this._activeTab="activity"),this._areaSubscribing=!1,this._areaUnsub&&(this._areaUnsub(),this._areaUnsub=null)}get _isFavoritesView(){return this._selectedPanelId===yn}_onTabClick(e){const t=e.target.closest(".shared-tab");if(!t)return;const n=t.dataset.tab;n&&n!==this._activeTab&&(this._activeTab=n,this._isFavoritesView&&"dashboard"!==n&&(this._favoritesViewState.activeTab=n,mn(this._favoritesViewState)))}_onTabContentClick(e){const t=e.target.closest(".unit-btn");if(t){const e=t.dataset.unit;if(!e||e===this._chartMetric)return;return this._chartMetric=e,void localStorage.setItem("span_panel_metric",e)}}_onSidePanelClosed(){if("dashboard"===this._activeTab){const e=this._dashboardTab._ctrl;e.monitoringCache.invalidate(),e.graphSettingsCache.invalidate()}this._listDashCtrl.monitoringMultiCache.invalidate(),this._pendingTabRender&&(this._pendingTabRender=!1,this._scheduleTabRender())}_onUnitChanged(e){const t=e.detail;t&&t!==this._chartMetric&&(this._chartMetric=t,localStorage.setItem("span_panel_metric",t))}_onListColumnsChanged(e){const t=e.detail;"number"!=typeof t||1!==t&&2!==t&&3!==t||t===this._listColumns||(this._listColumns=t,je(t))}_onGraphSettingsChanged(){if("dashboard"===this._activeTab){const e=this._root.getElementById("tab-content");if(e){this._dashboardTab._ctrl.onGraphSettingsChanged(e)}}}_onNavigateTab(e){const t=e.detail;t&&(this._activeTab=t)}_onFavoritesViewStateChangedEvent(e){if(!this._isFavoritesView)return;const t=e.detail;if(!t)return;const n=this._favoritesViewState;n.activeTab=t.view;const i=this._listDashCtrl.topology,s=i?.circuits;s&&Object.keys(s).length>0?n.expanded[t.view]=t.expanded.filter(e=>e in s):n.expanded[t.view]=t.expanded,n.searchQuery=t.searchQuery,this._persistFavoritesViewStateTimer&&clearTimeout(this._persistFavoritesViewStateTimer),this._persistFavoritesViewStateTimer=setTimeout(()=>{this._persistFavoritesViewStateTimer=null,mn(n)},250)}_subscribeDeviceRegistry(){!this._deviceRegistryUnsub&&this.hass?.connection&&(this._deviceRegistryUnsub=this.hass.connection.subscribeEvents(()=>this._refreshPanels(),"device_registry_updated"))}_unsubscribeDeviceRegistry(){this._deviceRegistryUnsub&&(this._deviceRegistryUnsub.then(e=>e()),this._deviceRegistryUnsub=null)}async _refreshPanels(){if(!this.hass||!this._discovered)return;const e=(await this.hass.callWS({type:"config/device_registry/list"})).filter(e=>e.identifiers?.some(e=>e[0]===a)&&!e.via_device_id),t=this._panels.filter(e=>e.id!==yn),n=new Map(t.map(e=>[e.id,e])),i=new Set(e.map(e=>e.id)),s=n.size!==i.size||[...n.keys()].some(e=>!i.has(e)),o=!s&&e.some(e=>{const t=n.get(e.id);return!!t&&(t.name!==e.name||t.name_by_user!==e.name_by_user)});if((s||o)&&(this._panels=this._buildPanelList(e,this._favorites),!this._panels.some(e=>e.id===this._selectedPanelId)&&this._panels.length>0)){const t=e[0];t&&(this._selectedPanelId=t.id,localStorage.setItem("span_panel_selected",this._selectedPanelId))}}async _updatePanelStatusWatch(){if(!this.hass||!this._selectedPanelId)return;if(this._selectedPanelId===yn)return;if(this._watchedPanelId===this._selectedPanelId)return;const e=this._selectedPanelId;this._watchedPanelId=e;try{const t=new We(this._errorStore),n=await Ye(this.hass,e,t);if(this._selectedPanelId!==e)return;const i=n.topology?.panel_entities?.panel_status;i&&(this._errorStore.watchPanelStatus(i),this._errorStore.updateHass(this.hass))}catch(t){console.warn("SPAN Panel: unable to fetch topology for panel status watching",t),this._watchedPanelId===e&&(this._watchedPanelId=null)}}async _discoverPanels(){if(!this._discovering&&this.hass){this._discovering=!0;try{let e;try{const t=new We(this._errorStore);e=(await t.callWS(this.hass,{type:"config/device_registry/list"},{errorId:"fetch:topology"})).filter(e=>e.identifiers?.some(e=>e[0]===a)&&!e.via_device_id)}catch(e){return console.error("SPAN Panel: device discovery failed",e),void this._errorStore.add({key:"discovery-failed",level:"error",message:n("error.discovery_failed"),persistent:!0,retryFn:()=>{this._errorStore.remove("discovery-failed"),this._discoverPanels()}})}this._favorites=await this._loadFavorites(),this._panels=this._buildPanelList(e,this._favorites),this._favoritesViewState=function(){try{const e=localStorage.getItem(vn);if(!e)return{expanded:{activity:[],area:[]}};const t=JSON.parse(e);if(!t||"object"!=typeof t)return{expanded:{activity:[],area:[]}};const n=t.expanded??{activity:[],area:[]};return{activeTab:t.activeTab,expanded:{activity:Array.isArray(n.activity)?n.activity:[],area:Array.isArray(n.area)?n.area:[]},searchQuery:"string"==typeof t.searchQuery?t.searchQuery:void 0}}catch{return{expanded:{activity:[],area:[]}}}}(),this._discovered=!0;const t=localStorage.getItem("span_panel_selected");if(t&&this._panels.some(e=>e.id===t)?this._selectedPanelId=t:e.length>0&&(this._selectedPanelId=e[0].id),this._selectedPanelId===yn){const e=this._favoritesViewState.activeTab;"activity"===e||"area"===e||"monitoring"===e?this._activeTab=e:"dashboard"===this._activeTab&&(this._activeTab="activity")}this._chartMetric=localStorage.getItem("span_panel_metric")||"power"}finally{this._discovering=!1}}}_buildPanelList(e,t){if(!Qe(t))return e;return[{id:yn,name:n("panel.favorites"),model:"__favorites__"},...e]}async _loadFavorites(){return this.hass?this._favCache.fetch(this.hass):{}}async _refreshFavorites(){const e=++this._refreshSeq;this._favCache.invalidate();const t=await this._loadFavorites();if(e!==this._refreshSeq)return;const n=this._selectedPanelId===yn;this._favorites=t;const i=this._panels.filter(e=>e.id!==yn);if(this._panels=this._buildPanelList(i,t),n&&!Qe(t)){!function(){try{localStorage.removeItem(vn)}catch{}}(),this._favoritesViewState={expanded:{activity:[],area:[]}};const e=i[0];e?(this._selectedPanelId=e.id,localStorage.setItem("span_panel_selected",e.id)):this._selectedPanelId=null}else this._isFavoritesView?this._scheduleTabRender():this._applyPanelFavorites()}_buildTabList(){const e=[];return this._isFavoritesView||e.push({id:"dashboard",label:n("tab.by_panel"),icon:"mdi:view-dashboard"}),e.push({id:"activity",label:n("tab.by_activity"),icon:"mdi:sort-descending"},{id:"area",label:n("tab.by_area"),icon:"mdi:home-group"},{id:"monitoring",label:n("tab.monitoring"),icon:"mdi:monitor-eye"}),e}_buildFavoritesSummaryHTML(){return function(e){return`\n \n
\n \n \n
\n
${Oe(n("header.enable_switches"))} \n
\n \n
\n
\n
\n ${et()}\n
\n W \n A \n
\n
\n
\n `}("current"===(this._chartMetric||"power"))}_buildFavoritesPanelStatsGridHTML(e,t){if(0===e.length)return"";return`${e.map(e=>`\n
\n
${Oe(e.panelName||e.topology.device_name||"")}
\n ${tt(e.topology,t,e.panelDeviceId)}\n
\n `).join("")}
`}_updateFavoritesPanelStats(e,t){if(this.hass&&0!==this._favoritesPanelStats.length)for(const n of this._favoritesPanelStats){const i=e.querySelector(`.panel-stats[data-stats-panel-id="${jt(n.panelDeviceId)}"]`);i&&Ut(i,this.hass,n.topology,t,0)}}_buildDashboardConfig(){return{chart_metric:this._chartMetric,history_minutes:5,show_panel:!0,show_battery:!0,show_evse:!0}}async _scheduleTabRender(){await this.updateComplete,this._sidePanelOpen()?this._pendingTabRender=!0:await this._tabRenderScheduler()}_sidePanelOpen(){const e=this.shadowRoot?.getElementById("tab-content");return!!e?.querySelector("span-side-panel[open]")}async _renderTab(){const e=this._beginRender();this._dashboardTab.stop(),this._monitoringTab.stop(),this._listCtrl.stop(),this._listDashCtrl.stopIntervals();for(const e of this._favoritesMonitoringTabs.values())e.stop();this._favoritesMonitoringTabs.clear(),this._favoritesPanelStats=[];const t=this._root.getElementById("tab-content");if(t)if(this._isFavoritesView)await this._renderFavoritesTab(t,e);else switch(this._listDashCtrl.clearFavoriteRefs(),this._listCtrl.setViewName(null),this._applyPanelFavorites(),this._activeTab){case"dashboard":{t.innerHTML="";const e=this._buildDashboardConfig(),n=this._panels.find(e=>e.id===this._selectedPanelId),i=n?.config_entries?.[0]??null;await this._dashboardTab.render(t,this.hass,this._selectedPanelId??"",e,i);break}case"activity":{t.innerHTML="";const n=this._panels.find(e=>e.id===this._selectedPanelId),i=n?.config_entries?.[0]??null;try{const n=new We(this._errorStore),s=await Ye(this.hass,this._selectedPanelId??void 0,n);if(e())return;const o=this._buildDashboardConfig();if(this._listDashCtrl.init(s.topology,o,this.hass,i),this._listDashCtrl.powerHistory.clear(),await this._listDashCtrl.monitoringCache.fetch(this.hass,i),e())return;if(await this._listDashCtrl.fetchAndBuildHorizonMaps(),e())return;const r=s.topology?nt(s.topology,o):"";if(this._listCtrl.setColumns(this._listColumns),this._listCtrl.renderActivityView(t,this.hass,s.topology,o,this._listDashCtrl.monitoringCache.status,r),await this._listDashCtrl.loadHistory(),e())return;this._listDashCtrl.updateDOM(t),this._listDashCtrl.startIntervals(t)}catch(n){if(e())return;const i=document.createElement("p");i.style.color="var(--error-color)",i.textContent=n.message,t.appendChild(i)}break}case"area":{t.innerHTML="";const i=this._panels.find(e=>e.id===this._selectedPanelId),s=i?.config_entries?.[0]??null;try{const i=new We(this._errorStore),o=await Ye(this.hass,this._selectedPanelId??void 0,i);if(e())return;const r=this._buildDashboardConfig();if(this._listDashCtrl.init(o.topology,r,this.hass,s),this._listDashCtrl.powerHistory.clear(),await this._listDashCtrl.monitoringCache.fetch(this.hass,s),e())return;if(await this._listDashCtrl.fetchAndBuildHorizonMaps(),e())return;const a=o.topology?nt(o.topology,r):"";if(this._listCtrl.setColumns(this._listColumns),this._listCtrl.renderAreaView(t,this.hass,o.topology,r,this._listDashCtrl.monitoringCache.status,a),await this._listDashCtrl.loadHistory(),e())return;this._listDashCtrl.updateDOM(t),this._listDashCtrl.startIntervals(t),this._areaUnsub||this._areaSubscribing||(this._areaSubscribing=!0,async function(e,t,i,s){if(!e.connection)return()=>{};const o=async()=>{try{const n=new Map;for(const[e,i]of Object.entries(t.circuits))n.set(e,i.area);await Ze(e,t);for(const[e,s]of Object.entries(t.circuits))if(s.area!==n.get(e))return void i()}catch(e){console.warn("[span-panel] area registry update failed:",e),s?.add({key:"fetch:areas",level:"warning",message:n("error.areas_failed"),persistent:!1})}},[r,a]=await Promise.all([e.connection.subscribeEvents(o,"entity_registry_updated"),e.connection.subscribeEvents(o,"area_registry_updated")]);return()=>{r(),a()}}(this.hass,o.topology,()=>{"area"===this._activeTab&&this._scheduleTabRender()},this._errorStore).then(e=>{this._areaSubscribing?this._areaUnsub=e:e()}).catch(e=>{this._areaSubscribing=!1,console.warn("SPAN Panel: area subscription failed",e),this._errorStore.add({key:"subscribe:area",level:"warning",message:n("error.areas_failed"),persistent:!1})}))}catch(e){const n=document.createElement("p");n.style.color="var(--error-color)",n.textContent=e instanceof Error?e.message:String(e),t.appendChild(n)}break}case"monitoring":{t.innerHTML="";const e=this._panels.find(e=>e.id===this._selectedPanelId),n=e?.config_entries?.[0]??null;await this._monitoringTab.render(t,this.hass,n??void 0);break}}}async _renderFavoritesTab(e,t){if(e.innerHTML="",!this.hass)return;const i=this._panels.filter(e=>e.id!==yn),s=await this._favCtrl.build(this.hass,this._favorites,i,this._errorStore);if(t())return;const o=s.perPanelStats.map(e=>{const t=e.topology.panel_entities?.panel_status;return"string"==typeof t?{entityId:t,panelName:e.panelName}:null}).filter(e=>null!==e);this._errorStore.watchPanelStatuses(o),this._errorStore.updateHass(this.hass);const r=new Map;for(const e of s.perPanelStats){const t=i.find(t=>t.id===e.panelDeviceId);r.set(e.panelDeviceId,{panelName:e.panelName,topology:e.topology,configEntryId:t?.config_entries?.[0]??null})}this._listDashCtrl.setFavoritesPerPanelInfo(r);const a=s.topology,l=s.entryIds[0]??null,c=Object.keys(a.circuits).length>0,d=Object.keys(a.sub_devices??{}).length>0;if(!c&&!d){const t=document.createElement("p");return t.style.color="var(--secondary-text-color)",t.style.padding="24px",t.textContent=n("list.no_results"),void e.appendChild(t)}if(this._listDashCtrl.setFavoriteRefs(a._favoriteRefs),this._listDashCtrl.setPanelFavorites(null),"monitoring"===this._activeTab)return this._listCtrl.setViewName(null),void await this._renderFavoritesMonitoring(e,s.entryIds,i);const h=this._activeTab,p=new Set(Object.keys(a.circuits)),u=this._favoritesViewState.expanded[h].filter(e=>p.has(e));this._listCtrl.setViewName(h),this._listCtrl.setInitialExpansion(u),this._listCtrl.setInitialSearchQuery(this._favoritesViewState.searchQuery??""),this._listCtrl.setColumns(this._listColumns);const g=this._buildDashboardConfig();if(this._listDashCtrl.init(a,g,this.hass,l),this._listDashCtrl.powerHistory.clear(),await this._listDashCtrl.fetchAndBuildHorizonMaps(),t())return;const _=await this._listDashCtrl.fetchMergedMonitoringStatus(s.entryIds);if(!t()){this._favoritesPanelStats=s.perPanelStats;try{if(await this._listDashCtrl.loadHistory(),t())return;const n=this._buildFavoritesSummaryHTML(),i=this._buildFavoritesPanelStatsGridHTML(s.perPanelStats,g),o=n+i+(d?`\n
${Et(a,this.hass,g)}
\n
`:"");"activity"===h?this._listCtrl.renderActivityView(e,this.hass,a,g,_,o):this._listCtrl.renderAreaView(e,this.hass,a,g,_,o),this._updateFavoritesPanelStats(e,g),this._listDashCtrl.setupResizeObserver(e,e),this._listDashCtrl.startIntervals(e,()=>{this._updateFavoritesPanelStats(e,g)})}catch(n){if(t())return;const i=document.createElement("p");i.style.color="var(--error-color)",i.textContent=n.message,e.appendChild(i)}}}async _renderFavoritesMonitoring(e,t,n){if(!this.hass)return;const i=document.createElement("div");i.className="favorites-monitoring-stack",e.appendChild(i);const s=new Map;for(const e of n){const t=e.config_entries?.[0];t&&s.set(t,e)}const o=new Map;for(const e of t){const t=s.get(e),n=document.createElement("div");n.className="favorites-monitoring-block",n.style.marginBottom="24px";const r=document.createElement("h2");r.style.margin="8px 0 12px",r.style.fontSize="1em",r.textContent=t?.name_by_user??t?.name??e,n.appendChild(r);const a=document.createElement("div");n.appendChild(a),i.appendChild(n);const l=new sn;l.errorStore=this._errorStore,o.set(e,l);try{await l.render(a,this.hass,e)}catch(t){console.warn("SPAN Panel: favorites monitoring render failed",e,t);const n=document.createElement("p");n.style.color="var(--error-color)",n.textContent=t.message??String(t),a.appendChild(n)}}this._favoritesMonitoringTabs=o}_applyPanelFavorites(){if(!this._selectedPanelId||this._isFavoritesView)return this._listDashCtrl.setPanelFavorites(null),void this._dashboardTab.setPanelFavorites(null);const e=this._favorites[this._selectedPanelId],t={panelDeviceId:this._selectedPanelId,circuitUuids:new Set(e?.circuits??[]),subDeviceIds:new Set(e?.sub_devices??[])};this._listDashCtrl.setPanelFavorites(t),this._dashboardTab.setPanelFavorites(t)}};wn._shellStyles=C`
:host {
color: var(--primary-text-color);
}
@@ -109,6 +175,10 @@ const Ce={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:H},Ee=(e=Ce
margin: 0 0 0 24px;
line-height: 20px;
flex-grow: 1;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ min-width: 0;
}
.panel-selector select {
color: inherit;
@@ -125,10 +195,13 @@ const Ce={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:H},Ee=(e=Ce
color: var(--primary-text-color);
}
.panel-tabs {
- margin-left: max(env(safe-area-inset-left), 24px);
- margin-right: max(env(safe-area-inset-right), 24px);
display: flex;
gap: 0;
+ overflow-x: auto;
+ scrollbar-width: none;
+ }
+ .panel-tabs::-webkit-scrollbar {
+ display: none;
}
.panel-tab {
padding: 8px 20px;
@@ -177,4 +250,4 @@ const Ce={attribute:!0,type:String,converter:L,reflect:!1,hasChanged:H},Ee=(e=Ce
opacity: 1;
border-bottom-color: var(--app-header-text-color, white);
}
- `,m([ke({attribute:!1})],Wt.prototype,"hass",void 0),m([ke({type:Boolean,reflect:!0})],Wt.prototype,"narrow",void 0),m([ze()],Wt.prototype,"_panels",void 0),m([ze()],Wt.prototype,"_selectedPanelId",void 0),m([ze()],Wt.prototype,"_activeTab",void 0),m([ze()],Wt.prototype,"_discovered",void 0),m([ze()],Wt.prototype,"_discoveryError",void 0),m([ze()],Wt.prototype,"_chartMetric",void 0),Wt=m([(e=>(t,n)=>{void 0!==n?n.addInitializer(()=>{customElements.define(e,t)}):customElements.define(e,t)})("span-panel")],Wt),console.warn("%c SPAN-PANEL %c v0.9.2 ","background: var(--primary-color, #4dd9af); color: #000; font-weight: 700; padding: 2px 6px; border-radius: 4px 0 0 4px;","background: var(--secondary-background-color, #333); color: var(--primary-text-color, #fff); padding: 2px 6px; border-radius: 0 4px 4px 0;");let Bt=!1;const Vt=Wt.prototype.connectedCallback;Wt.prototype.connectedCallback=function(){Bt=!0,Vt.call(this)};const Qt=Wt.prototype.disconnectedCallback;Wt.prototype.disconnectedCallback=function(){Bt=!1,Qt.call(this)},document.addEventListener("visibilitychange",()=>{"visible"===document.visibilityState&&(Bt||window.location.pathname.includes("span-panel")&&setTimeout(()=>{Bt||location.reload()},200))});
+ `,wn.styles=[bn._shellStyles,$(Kt)],m([Ne({attribute:!1})],wn.prototype,"hass",void 0),m([Ne({type:Boolean,reflect:!0})],wn.prototype,"narrow",void 0),m([Me()],wn.prototype,"_panels",void 0),m([Me()],wn.prototype,"_selectedPanelId",void 0),m([Me()],wn.prototype,"_activeTab",void 0),m([Me()],wn.prototype,"_discovered",void 0),m([Me()],wn.prototype,"_chartMetric",void 0),m([Me()],wn.prototype,"_listColumns",void 0),m([Me()],wn.prototype,"_favorites",void 0),wn=bn=m([Ee("span-panel")],wn),console.warn("%c SPAN-PANEL %c v0.9.4 ","background: var(--primary-color, #4dd9af); color: #000; font-weight: 700; padding: 2px 6px; border-radius: 4px 0 0 4px;","background: var(--secondary-background-color, #333); color: var(--primary-text-color, #fff); padding: 2px 6px; border-radius: 0 4px 4px 0;");let xn=!1;const Sn=wn.prototype.connectedCallback;wn.prototype.connectedCallback=function(){xn=!0,Sn.call(this)};const $n=wn.prototype.disconnectedCallback;wn.prototype.disconnectedCallback=function(){xn=!1,$n.call(this)},document.addEventListener("visibilitychange",()=>{"visible"===document.visibilityState&&(xn||window.location.pathname.includes("span-panel")&&setTimeout(()=>{xn||location.reload()},200))});
diff --git a/custom_components/span_panel/grace_period.py b/custom_components/span_panel/grace_period.py
index b4b5a906..aa499174 100644
--- a/custom_components/span_panel/grace_period.py
+++ b/custom_components/span_panel/grace_period.py
@@ -3,7 +3,8 @@
from __future__ import annotations
from dataclasses import dataclass
-from datetime import UTC, datetime, timedelta
+from datetime import UTC, date, datetime, timedelta
+from decimal import Decimal
import logging
from typing import Any, Self
@@ -11,6 +12,11 @@
from homeassistant.core import State
from homeassistant.helpers.restore_state import ExtraStoredData
+# Matches SensorEntity.native_value: the broadest concrete union that the
+# HA sensor platform can hand us. We accept it verbatim so sensor_base can
+# pass `self._attr_native_value` through without extra narrowing.
+type NativeSensorValue = str | int | float | date | Decimal | None
+
_LOGGER = logging.getLogger(__name__)
@@ -96,7 +102,7 @@ def from_dict(cls, restored: dict[str, Any]) -> Self | None:
return None
-def coerce_grace_period_minutes(raw_value: Any) -> int:
+def coerce_grace_period_minutes(raw_value: int | float | str | None) -> int:
"""Ensure grace period minutes is a non-negative integer.
Args:
@@ -106,6 +112,8 @@ def coerce_grace_period_minutes(raw_value: Any) -> int:
Validated integer (defaults to 15 if invalid, clamps to 0 minimum).
"""
+ if raw_value is None:
+ return 15
try:
minutes = int(raw_value)
except (TypeError, ValueError):
@@ -120,9 +128,9 @@ def coerce_grace_period_minutes(raw_value: Any) -> int:
def handle_offline_grace_period(
last_valid_state: float | None,
last_valid_changed: datetime | None,
- current_native_value: Any,
+ current_native_value: NativeSensorValue,
grace_minutes: int,
-) -> tuple[Any, float | None, datetime | None]:
+) -> tuple[NativeSensorValue, float | None, datetime | None]:
"""Handle grace period logic when panel is offline.
Args:
@@ -150,7 +158,11 @@ def handle_offline_grace_period(
last_valid_changed = datetime.now(tz=UTC)
try:
- time_since_last_valid = datetime.now(tz=UTC) - last_valid_changed
+ raw_delta = datetime.now(tz=UTC) - last_valid_changed
+ # Clamp to zero to handle backward clock jumps (DST / NTP sync) that would
+ # otherwise produce a negative delta and silently extend the grace window
+ # indefinitely.
+ time_since_last_valid = max(raw_delta, timedelta(0))
grace_period_duration = timedelta(minutes=grace_minutes)
except Exception as err: # noqa: BLE001 # pragma: no cover - defensive
_LOGGER.debug("Grace period calculation failed: %s", err)
diff --git a/custom_components/span_panel/graph_horizon.py b/custom_components/span_panel/graph_horizon.py
index 7116ede2..5090ddc7 100644
--- a/custom_components/span_panel/graph_horizon.py
+++ b/custom_components/span_panel/graph_horizon.py
@@ -33,7 +33,7 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
self._global_horizon: str = DEFAULT_GRAPH_HORIZON
self._circuit_overrides: dict[str, str] = {}
self._subdevice_overrides: dict[str, str] = {}
- self._store: Store = Store(
+ self._store: Store[dict[str, Any]] = Store(
hass,
_STORAGE_VERSION,
f"{_STORAGE_KEY_PREFIX}.{entry.entry_id}",
diff --git a/custom_components/span_panel/helpers.py b/custom_components/span_panel/helpers.py
index 017d5453..2620c82b 100644
--- a/custom_components/span_panel/helpers.py
+++ b/custom_components/span_panel/helpers.py
@@ -52,6 +52,55 @@
is_panel_level_sensor_key,
)
+__all__ = [
+ "ALL_SUFFIX_MAPPINGS",
+ "CIRCUIT_SUFFIX_MAPPING",
+ "PANEL_ENTITY_SUFFIX_MAPPING",
+ "PANEL_SUFFIX_MAPPING",
+ "async_create_span_notification",
+ "build_bess_unique_id",
+ "build_bess_unique_id_for_entry",
+ "build_binary_sensor_unique_id",
+ "build_binary_sensor_unique_id_for_entry",
+ "build_circuit_unique_id",
+ "build_evse_unique_id",
+ "build_evse_unique_id_for_entry",
+ "build_panel_unique_id",
+ "build_select_unique_id",
+ "build_select_unique_id_for_entry",
+ "build_switch_unique_id",
+ "build_switch_unique_id_for_entry",
+ "construct_binary_sensor_unique_id",
+ "construct_circuit_identifier_from_tabs",
+ "construct_circuit_unique_id",
+ "construct_circuit_unique_id_for_entry",
+ "construct_multi_circuit_entity_id",
+ "construct_panel_unique_id",
+ "construct_panel_unique_id_for_entry",
+ "construct_select_unique_id",
+ "construct_single_circuit_entity_id",
+ "construct_switch_unique_id",
+ "construct_synthetic_unique_id",
+ "construct_synthetic_unique_id_for_entry",
+ "construct_tabs_attribute",
+ "construct_unmapped_entity_id",
+ "construct_unmapped_friendly_name",
+ "construct_voltage_attribute",
+ "detect_capabilities",
+ "er",
+ "get_device_identifier_for_entry",
+ "get_panel_entity_suffix",
+ "get_suffix_from_sensor_key",
+ "get_unmapped_circuit_entity_id",
+ "get_user_friendly_suffix",
+ "has_bess",
+ "has_evse",
+ "has_power_flows",
+ "has_pv",
+ "is_panel_level_sensor_key",
+ "resolve_evse_display_suffix",
+]
+
_LOGGER = logging.getLogger(__name__)
diff --git a/custom_components/span_panel/icons.json b/custom_components/span_panel/icons.json
index 7c410923..4a7e9445 100644
--- a/custom_components/span_panel/icons.json
+++ b/custom_components/span_panel/icons.json
@@ -115,9 +115,18 @@
"faulted": "mdi:alert-circle"
}
},
+ "feedthrough_consumed_energy": {
+ "default": "mdi:transmission-tower-import"
+ },
+ "feedthrough_net_energy": {
+ "default": "mdi:swap-horizontal-bold"
+ },
"feedthrough_power": {
"default": "mdi:flash"
},
+ "feedthrough_produced_energy": {
+ "default": "mdi:transmission-tower-export"
+ },
"grid_forming_entity": {
"default": "mdi:lightning-bolt-circle",
"state": {
@@ -143,6 +152,15 @@
"main_breaker_rating": {
"default": "mdi:fuse"
},
+ "main_meter_consumed_energy": {
+ "default": "mdi:meter-electric"
+ },
+ "main_meter_net_energy": {
+ "default": "mdi:swap-horizontal-bold"
+ },
+ "main_meter_produced_energy": {
+ "default": "mdi:meter-electric-outline"
+ },
"main_relay_state": {
"default": "mdi:electric-switch",
"state": {
diff --git a/custom_components/span_panel/id_builder.py b/custom_components/span_panel/id_builder.py
index 57509984..b790883c 100644
--- a/custom_components/span_panel/id_builder.py
+++ b/custom_components/span_panel/id_builder.py
@@ -187,6 +187,52 @@ def get_panel_entity_suffix(description_key: str) -> str:
return get_user_friendly_suffix(description_key)
+def extract_circuit_uuid_from_unique_id(unique_id: str) -> str | None:
+ """Return the 32-char hex circuit UUID embedded in a SPAN entity unique_id.
+
+ SPAN entity unique_ids follow ``span_{serial}_{circuit_uuid}_{suffix}``.
+ The circuit UUID is a 32-char lowercase hex segment. Skips ``parts[0]``
+ (``span``) and ``parts[1]`` (the serial — never a circuit UUID) so a
+ serial that happens to be 32 hex chars cannot shadow the circuit id.
+
+ Returns ``None`` for unique_ids with no circuit UUID segment (e.g.
+ panel-level sensors).
+ """
+ if not unique_id:
+ return None
+ parts = unique_id.split("_")
+ for part in parts[2:]:
+ if len(part) == 32 and all(c in "0123456789abcdef" for c in part):
+ return part
+ return None
+
+
+# ---------------------------------------------------------------------------
+# build_*_unique_id — DO NOT "normalise" without a migration path.
+#
+# Historically, build_circuit_unique_id, build_panel_unique_id, and
+# construct_synthetic_unique_id lower-case the serial; build_switch_unique_id,
+# build_binary_sensor_unique_id, build_select_unique_id, build_bess_unique_id,
+# and build_evse_unique_id do NOT. On panels with a mixed-case serial this
+# means a single install has some unique_ids with the serial lower-cased and
+# others with the serial preserved as-is.
+#
+# This is an inconsistency, but it is benign: HA's entity registry keys on
+# string equality, not case-folded equality, so every deployed entity still
+# matches itself on every restart. Unifying the casing would silently rewrite
+# what these functions return and orphan every existing switch, binary_sensor,
+# select, BESS, and EVSE entity on any live install whose serial contains
+# upper-case characters. The registry would then recreate those entities under
+# the new unique_ids with default names — breaking dashboards, automations,
+# and statistics history.
+#
+# If you ever truly need to align these, do it together with a config-entry
+# migration that walks the registry and renames stored unique_ids to match
+# the new convention. Do not change a single one of these return strings in
+# isolation.
+# ---------------------------------------------------------------------------
+
+
def build_circuit_unique_id(serial: str, circuit_id: str, description_key: str) -> str:
"""Build unique ID for circuit sensors using consistent pattern (pure function).
diff --git a/custom_components/span_panel/manifest.json b/custom_components/span_panel/manifest.json
index e1632f9e..ac5e6427 100644
--- a/custom_components/span_panel/manifest.json
+++ b/custom_components/span_panel/manifest.json
@@ -22,7 +22,7 @@
],
"quality_scale": "gold",
"requirements": [
- "span-panel-api==2.5.4"
+ "span-panel-api==2.6.1"
],
"version": "2.0.6",
"zeroconf": [
diff --git a/custom_components/span_panel/select.py b/custom_components/span_panel/select.py
index b115c91a..30f54042 100644
--- a/custom_components/span_panel/select.py
+++ b/custom_components/span_panel/select.py
@@ -8,7 +8,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceNotFound
from homeassistant.helpers import entity_registry as er
-from homeassistant.helpers.entity import EntityCategory # type: ignore[attr-defined]
+from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from span_panel_api import SpanCircuitSnapshot, SpanPanelSnapshot
from span_panel_api.exceptions import SpanPanelServerError
diff --git a/custom_components/span_panel/sensor_definitions.py b/custom_components/span_panel/sensor_definitions.py
index 3d3b3e7b..e94d3ac6 100644
--- a/custom_components/span_panel/sensor_definitions.py
+++ b/custom_components/span_panel/sensor_definitions.py
@@ -26,7 +26,7 @@
UnitOfEnergy,
UnitOfPower,
)
-from homeassistant.helpers.entity import EntityCategory # type: ignore[attr-defined]
+from homeassistant.helpers.entity import EntityCategory
from span_panel_api import (
SpanBatterySnapshot,
SpanCircuitSnapshot,
diff --git a/custom_components/span_panel/services.py b/custom_components/span_panel/services.py
index 458beb4c..35699b25 100644
--- a/custom_components/span_panel/services.py
+++ b/custom_components/span_panel/services.py
@@ -8,13 +8,14 @@
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse, SupportsResponse
from homeassistant.exceptions import ServiceValidationError
-from homeassistant.helpers import entity_registry as er
+from homeassistant.helpers import device_registry as dr, entity_registry as er
import voluptuous as vol
from .const import DEFAULT_GRAPH_HORIZON, DOMAIN, VALID_GRAPH_HORIZONS
from .current_monitor import CurrentMonitor
+from .frontend import FavoriteKind, async_get_favorites, async_set_favorite
from .graph_horizon import GraphHorizonManager
-from .id_builder import build_circuit_unique_id
+from .id_builder import build_circuit_unique_id, extract_circuit_uuid_from_unique_id
from .options import (
CONTINUOUS_THRESHOLD_PCT,
COOLDOWN_DURATION_M,
@@ -496,3 +497,145 @@ async def async_handle_get_graph_settings(
schema=vol.Schema({vol.Optional("config_entry_id"): str}),
supports_response=SupportsResponse.ONLY,
)
+
+
+def _async_register_favorites_services(hass: HomeAssistant) -> None:
+ """Register cross-panel favorites services (domain-level).
+
+ The public API takes ``entity_id`` — any sensor on a SPAN circuit or
+ sub-device — and resolves it server-side to the internal
+ ``(panel_device_id, kind, target_id)`` tuple used in storage. Circuit
+ UUIDs and HA device IDs are not part of the user-visible surface.
+ """
+
+ def _resolve_entity_to_favorite_target(entity_id: str) -> tuple[str, FavoriteKind, str]:
+ """Return ``(panel_device_id, kind, target_id)`` for a SPAN entity.
+
+ ``kind`` is ``"circuits"`` or ``"sub_devices"``. For circuits,
+ ``target_id`` is the panel-local circuit uuid (extracted from the
+ entity's unique_id). For sub-devices, ``target_id`` is the HA
+ device id of the BESS/EVSE; the panel id walks up via ``via_device_id``.
+
+ Failure paths use distinct translation keys so users see the
+ actual reason their pick was rejected.
+ """
+ entity_reg = er.async_get(hass)
+ entry = entity_reg.async_get(entity_id)
+ if entry is None or entry.platform != DOMAIN:
+ raise ServiceValidationError(
+ f"Entity {entity_id} is not a SPAN Panel entity.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_not_span_entity",
+ translation_placeholders={"entity_id": entity_id},
+ )
+
+ if entry.device_id is None:
+ raise ServiceValidationError(
+ f"Entity {entity_id} is not attached to a device.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_no_device",
+ translation_placeholders={"entity_id": entity_id},
+ )
+
+ device_registry = dr.async_get(hass)
+ device_entry = device_registry.async_get(entry.device_id)
+ if device_entry is None or not any(
+ domain == DOMAIN for domain, _ in device_entry.identifiers
+ ):
+ raise ServiceValidationError(
+ f"Entity {entity_id} does not belong to a SPAN Panel device.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_not_span_entity",
+ translation_placeholders={"entity_id": entity_id},
+ )
+
+ # Resolve the panel device id. Sub-devices register with
+ # via_device_id; main panels never do, so via_device_id presence is a
+ # reliable discriminator (BESS / EVSE today) and we walk up to the
+ # parent SPAN Panel here.
+ if device_entry.via_device_id is not None:
+ parent = device_registry.async_get(device_entry.via_device_id)
+ if parent is None or not any(domain == DOMAIN for domain, _ in parent.identifiers):
+ raise ServiceValidationError(
+ f"Sub-device {entity_id} has no SPAN Panel parent.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_subdevice_no_span_parent",
+ translation_placeholders={"entity_id": entity_id},
+ )
+ panel_device_id = parent.id
+ else:
+ panel_device_id = device_entry.id
+
+ # Circuit-favorite branch takes precedence over the sub-device branch.
+ # EVSE feed-circuit sensors are re-assigned to the EVSE sub-device via
+ # the device override in sensor.py, but their unique_id still encodes
+ # the underlying circuit. When the unique_id embeds a 32-char circuit
+ # UUID (``span_{serial}_{circuit_uuid}_{suffix}``), favorite the
+ # circuit keyed by the parent panel — not the sub-device it happens
+ # to be attached to.
+ circuit_uuid = (
+ extract_circuit_uuid_from_unique_id(entry.unique_id) if entry.unique_id else None
+ )
+ if circuit_uuid is not None:
+ return panel_device_id, "circuits", circuit_uuid
+
+ # No circuit UUID — sub-device metadata sensor (BESS %, EVSE status,
+ # etc.). Favorite the sub-device itself.
+ if device_entry.via_device_id is not None:
+ return panel_device_id, "sub_devices", device_entry.id
+
+ # Main-panel entity without a circuit UUID — not favoritable.
+ if not entry.unique_id:
+ raise ServiceValidationError(
+ f"Entity {entity_id} has no unique id to resolve.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_no_unique_id",
+ translation_placeholders={"entity_id": entity_id},
+ )
+ raise ServiceValidationError(
+ f"Could not derive a favorite target from entity {entity_id}. "
+ "Pick a circuit sensor (current/power) or a sub-device sensor.",
+ translation_domain=DOMAIN,
+ translation_key="favorite_no_circuit_uuid",
+ translation_placeholders={"entity_id": entity_id},
+ )
+
+ async def async_handle_get_favorites(_call: ServiceCall) -> ServiceResponse:
+ favorites = await async_get_favorites(hass)
+ return cast(ServiceResponse, {"favorites": favorites})
+
+ async def async_handle_add_favorite(call: ServiceCall) -> ServiceResponse:
+ entity_id = call.data["entity_id"]
+ panel_device_id, kind, target_id = _resolve_entity_to_favorite_target(entity_id)
+ favorites = await async_set_favorite(hass, panel_device_id, kind, target_id, True)
+ return cast(ServiceResponse, {"favorites": favorites})
+
+ async def async_handle_remove_favorite(call: ServiceCall) -> ServiceResponse:
+ entity_id = call.data["entity_id"]
+ panel_device_id, kind, target_id = _resolve_entity_to_favorite_target(entity_id)
+ favorites = await async_set_favorite(hass, panel_device_id, kind, target_id, False)
+ return cast(ServiceResponse, {"favorites": favorites})
+
+ _favorite_mutation_schema = vol.Schema({vol.Required("entity_id"): str})
+
+ hass.services.async_register(
+ DOMAIN,
+ "get_favorites",
+ async_handle_get_favorites,
+ schema=vol.Schema({}),
+ supports_response=SupportsResponse.ONLY,
+ )
+ hass.services.async_register(
+ DOMAIN,
+ "add_favorite",
+ async_handle_add_favorite,
+ schema=_favorite_mutation_schema,
+ supports_response=SupportsResponse.OPTIONAL,
+ )
+ hass.services.async_register(
+ DOMAIN,
+ "remove_favorite",
+ async_handle_remove_favorite,
+ schema=_favorite_mutation_schema,
+ supports_response=SupportsResponse.OPTIONAL,
+ )
diff --git a/custom_components/span_panel/services.yaml b/custom_components/span_panel/services.yaml
index 65a068a4..530b9fcb 100644
--- a/custom_components/span_panel/services.yaml
+++ b/custom_components/span_panel/services.yaml
@@ -36,6 +36,10 @@ set_circuit_threshold:
monitoring_enabled:
selector:
boolean:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
clear_circuit_threshold:
fields:
@@ -46,6 +50,10 @@ clear_circuit_threshold:
domain: sensor
integration: span_panel
device_class: current
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
set_mains_threshold:
fields:
@@ -83,6 +91,10 @@ set_mains_threshold:
monitoring_enabled:
selector:
boolean:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
clear_mains_threshold:
fields:
@@ -93,8 +105,17 @@ clear_mains_threshold:
domain: sensor
integration: span_panel
device_class: current
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
get_monitoring_status:
+ fields:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
set_global_monitoring:
fields:
@@ -143,8 +164,17 @@ set_global_monitoring:
- "active"
- "time-sensitive"
- "critical"
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
test_notification:
+ fields:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
set_graph_time_horizon:
fields:
@@ -158,6 +188,10 @@ set_graph_time_horizon:
- "1d"
- "1w"
- "1M"
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
set_circuit_graph_horizon:
fields:
@@ -175,6 +209,10 @@ set_circuit_graph_horizon:
- "1d"
- "1w"
- "1M"
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
clear_circuit_graph_horizon:
fields:
@@ -182,6 +220,10 @@ clear_circuit_graph_horizon:
required: true
selector:
text:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
set_subdevice_graph_horizon:
fields:
@@ -199,6 +241,10 @@ set_subdevice_graph_horizon:
- "1d"
- "1w"
- "1M"
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
clear_subdevice_graph_horizon:
fields:
@@ -206,5 +252,34 @@ clear_subdevice_graph_horizon:
required: true
selector:
text:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
get_graph_settings:
+ fields:
+ config_entry_id:
+ selector:
+ config_entry:
+ integration: span_panel
+
+get_favorites:
+
+add_favorite:
+ fields:
+ entity_id:
+ required: true
+ selector:
+ entity:
+ domain: sensor
+ integration: span_panel
+
+remove_favorite:
+ fields:
+ entity_id:
+ required: true
+ selector:
+ entity:
+ domain: sensor
+ integration: span_panel
diff --git a/custom_components/span_panel/strings.json b/custom_components/span_panel/strings.json
index c8d70e19..e374babf 100644
--- a/custom_components/span_panel/strings.json
+++ b/custom_components/span_panel/strings.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "No SPAN panel configuration entries are loaded. Add and configure a SPAN panel before calling this service."
},
+ "favorite_not_span_entity": {
+ "message": "Entity {entity_id} is not a SPAN Panel entity. Pick a circuit, BESS, or EVSE sensor."
+ },
+ "favorite_no_device": {
+ "message": "Entity {entity_id} is not attached to a device. Pick a SPAN circuit or sub-device sensor."
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "Sub-device {entity_id} has no SPAN Panel parent device — its via_device_id does not point at a SPAN panel."
+ },
+ "favorite_no_unique_id": {
+ "message": "Entity {entity_id} has no unique id and cannot be resolved to a SPAN target."
+ },
+ "favorite_no_circuit_uuid": {
+ "message": "Could not derive a favorite target from entity {entity_id}. Pick a circuit sensor (current/power) or a sub-device sensor."
+ },
"gfe_override_failed": {
"message": "Failed to send GFE override to the panel."
},
+ "graph_horizon_not_available": {
+ "message": "Graph horizon manager is not available for the selected SPAN panel."
+ },
+ "monitoring_not_enabled": {
+ "message": "No SPAN panel with current monitoring enabled."
+ },
"panel_auth_failed": {
"message": "Authentication with the SPAN Panel failed. Please reauthenticate."
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "Monitoring enabled",
"description": "Enable or disable monitoring for this circuit."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "Circuit",
"description": "The circuit power sensor entity."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "Monitoring enabled",
"description": "Enable or disable monitoring for this mains leg."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "Mains leg",
"description": "The mains current sensor entity."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"get_monitoring_status": {
"name": "Get monitoring status",
- "description": "Returns current monitoring state for all tracked circuits and mains legs."
+ "description": "Returns current monitoring state for all tracked circuits and mains legs.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
},
"set_global_monitoring": {
"name": "Set Global Monitoring",
"description": "Update global monitoring thresholds and notification settings.",
"fields": {
+ "enabled": {
+ "name": "Enabled",
+ "description": "Enable or disable current monitoring globally for this SPAN panel."
+ },
"continuous_threshold_pct": {
"name": "Continuous Threshold",
"description": "Percentage of breaker rating for continuous overload detection."
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "Notification Priority",
"description": "Push notification priority level. Controls interruption behavior on mobile devices (iOS: interruption-level, Android: priority/channel)."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"test_notification": {
"name": "Test Notification",
- "description": "Send a test notification to all configured targets using sample values."
+ "description": "Send a test notification to all configured targets using sample values.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
},
"set_graph_time_horizon": {
"name": "Set graph time horizon",
@@ -532,6 +589,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "Circuit ID",
"description": "The circuit identifier to clear the override for."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "Sub-device ID",
"description": "The sub-device identifier to clear the override for."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"get_graph_settings": {
"name": "Get graph settings",
- "description": "Returns the current global and per-circuit graph horizon settings."
+ "description": "Returns the current global and per-circuit graph horizon settings.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "Get favorites",
+ "description": "Returns the saved cross-panel favorites map keyed by SPAN panel device id."
+ },
+ "add_favorite": {
+ "name": "Add favorite",
+ "description": "Mark a SPAN circuit, battery (BESS), or EV charger (EVSE) as a favorite so it appears in the dashboard's Favorites view.",
+ "fields": {
+ "entity_id": {
+ "name": "Entity",
+ "description": "Any sensor of the circuit or sub-device to favorite (current, power, SoC, etc.)."
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "Remove favorite",
+ "description": "Remove a circuit or sub-device from the cross-panel Favorites view.",
+ "fields": {
+ "entity_id": {
+ "name": "Entity",
+ "description": "Any sensor of the circuit or sub-device to remove from favorites."
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/threshold_evaluator.py b/custom_components/span_panel/threshold_evaluator.py
index 0289dd91..70c2509e 100644
--- a/custom_components/span_panel/threshold_evaluator.py
+++ b/custom_components/span_panel/threshold_evaluator.py
@@ -9,7 +9,7 @@
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING
from .const import (
DEFAULT_CONTINUOUS_THRESHOLD_PCT,
@@ -28,6 +28,16 @@
from .current_monitor import MonitoredPointState
+# Monitoring settings / overrides hold only int, bool, and str values.
+# Keyed by the named constants in options.py (`"continuous_threshold_pct"`,
+# `"monitoring_enabled"`, …) rather than arbitrary strings; a TypedDict would
+# express that more precisely but requires Literal-typed constants throughout
+# the call chain, so we keep the lighter-weight alias and rely on the named
+# keys for discoverability.
+type MonitoringValue = int | bool | str
+type MonitoringSettings = dict[str, MonitoringValue]
+
+
@dataclass
class AlertEvent:
"""Describes an alert to be dispatched."""
@@ -42,8 +52,8 @@ class AlertEvent:
def resolve_thresholds(
- override: dict[str, Any],
- global_settings: dict[str, Any],
+ override: MonitoringSettings,
+ global_settings: MonitoringSettings,
) -> tuple[int, int, int, int]:
"""Return (continuous_pct, spike_pct, window_m, cooldown_m) for a monitored point.
@@ -51,26 +61,34 @@ def resolve_thresholds(
defaults when neither layer provides a value.
"""
return (
- override.get(
- CONTINUOUS_THRESHOLD_PCT,
- global_settings.get(CONTINUOUS_THRESHOLD_PCT, DEFAULT_CONTINUOUS_THRESHOLD_PCT),
+ int(
+ override.get(
+ CONTINUOUS_THRESHOLD_PCT,
+ global_settings.get(CONTINUOUS_THRESHOLD_PCT, DEFAULT_CONTINUOUS_THRESHOLD_PCT),
+ )
),
- override.get(
- SPIKE_THRESHOLD_PCT,
- global_settings.get(SPIKE_THRESHOLD_PCT, DEFAULT_SPIKE_THRESHOLD_PCT),
+ int(
+ override.get(
+ SPIKE_THRESHOLD_PCT,
+ global_settings.get(SPIKE_THRESHOLD_PCT, DEFAULT_SPIKE_THRESHOLD_PCT),
+ )
),
- override.get(
- WINDOW_DURATION_M,
- global_settings.get(WINDOW_DURATION_M, DEFAULT_WINDOW_DURATION_M),
+ int(
+ override.get(
+ WINDOW_DURATION_M,
+ global_settings.get(WINDOW_DURATION_M, DEFAULT_WINDOW_DURATION_M),
+ )
),
- override.get(
- COOLDOWN_DURATION_M,
- global_settings.get(COOLDOWN_DURATION_M, DEFAULT_COOLDOWN_DURATION_M),
+ int(
+ override.get(
+ COOLDOWN_DURATION_M,
+ global_settings.get(COOLDOWN_DURATION_M, DEFAULT_COOLDOWN_DURATION_M),
+ )
),
)
-def is_monitoring_disabled(override: dict[str, Any]) -> bool:
+def is_monitoring_disabled(override: MonitoringSettings) -> bool:
"""Check if monitoring is disabled via per-point override."""
return override.get("monitoring_enabled") is False
diff --git a/custom_components/span_panel/translations/en.json b/custom_components/span_panel/translations/en.json
index 6c343ed5..040332a9 100644
--- a/custom_components/span_panel/translations/en.json
+++ b/custom_components/span_panel/translations/en.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "No SPAN panel configuration entries are loaded. Add and configure a SPAN panel before calling this service."
},
+ "favorite_not_span_entity": {
+ "message": "Entity {entity_id} is not a SPAN Panel entity. Pick a circuit, BESS, or EVSE sensor."
+ },
+ "favorite_no_device": {
+ "message": "Entity {entity_id} is not attached to a device. Pick a SPAN circuit or sub-device sensor."
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "Sub-device {entity_id} has no SPAN Panel parent device — its via_device_id does not point at a SPAN panel."
+ },
+ "favorite_no_unique_id": {
+ "message": "Entity {entity_id} has no unique id and cannot be resolved to a SPAN target."
+ },
+ "favorite_no_circuit_uuid": {
+ "message": "Could not derive a favorite target from entity {entity_id}. Pick a circuit sensor (current/power) or a sub-device sensor."
+ },
"gfe_override_failed": {
"message": "Failed to send GFE override to the panel."
},
+ "graph_horizon_not_available": {
+ "message": "Graph horizon manager is not available for the selected SPAN panel."
+ },
+ "monitoring_not_enabled": {
+ "message": "No SPAN panel with current monitoring enabled."
+ },
"panel_auth_failed": {
"message": "Authentication with the SPAN Panel failed. Please reauthenticate."
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "Monitoring enabled",
"description": "Enable or disable monitoring for this circuit."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "Circuit",
"description": "The circuit power sensor entity."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "Monitoring enabled",
"description": "Enable or disable monitoring for this mains leg."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "Mains leg",
"description": "The mains current sensor entity."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"get_monitoring_status": {
"name": "Get monitoring status",
- "description": "Returns current monitoring state for all tracked circuits and mains legs."
+ "description": "Returns current monitoring state for all tracked circuits and mains legs.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
},
"set_global_monitoring": {
"name": "Set Global Monitoring",
"description": "Update global monitoring thresholds and notification settings.",
"fields": {
+ "enabled": {
+ "name": "Enabled",
+ "description": "Enable or disable current monitoring globally for this SPAN panel."
+ },
"continuous_threshold_pct": {
"name": "Continuous Threshold",
"description": "Percentage of breaker rating for continuous overload detection."
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "Notification Priority",
"description": "Push notification priority level. Controls interruption behavior on mobile devices (iOS: interruption-level, Android: priority/channel)."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"test_notification": {
"name": "Test Notification",
- "description": "Send a test notification to all configured targets using sample values."
+ "description": "Send a test notification to all configured targets using sample values.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
},
"set_graph_time_horizon": {
"name": "Set graph time horizon",
@@ -532,6 +589,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "Circuit ID",
"description": "The circuit identifier to clear the override for."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "Horizon",
"description": "Time window preset for graph display."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "Sub-device ID",
"description": "The sub-device identifier to clear the override for."
+ },
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
}
}
},
"get_graph_settings": {
"name": "Get graph settings",
- "description": "Returns the current global and per-circuit graph horizon settings."
+ "description": "Returns the current global and per-circuit graph horizon settings.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Config entry",
+ "description": "Optional config entry id. Only needed when more than one SPAN panel is configured."
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "Get favorites",
+ "description": "Returns the saved cross-panel favorites map keyed by SPAN panel device id."
+ },
+ "add_favorite": {
+ "name": "Add favorite",
+ "description": "Mark a SPAN circuit, battery (BESS), or EV charger (EVSE) as a favorite so it appears in the dashboard's Favorites view.",
+ "fields": {
+ "entity_id": {
+ "name": "Entity",
+ "description": "Any sensor of the circuit or sub-device to favorite (current, power, SoC, etc.)."
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "Remove favorite",
+ "description": "Remove a circuit or sub-device from the cross-panel Favorites view.",
+ "fields": {
+ "entity_id": {
+ "name": "Entity",
+ "description": "Any sensor of the circuit or sub-device to remove from favorites."
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/translations/es.json b/custom_components/span_panel/translations/es.json
index 3b1168c8..3bb65e2e 100644
--- a/custom_components/span_panel/translations/es.json
+++ b/custom_components/span_panel/translations/es.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "No hay entradas de configuración de SPAN Panel cargadas. Agregue y configure un SPAN Panel antes de llamar a este servicio."
},
+ "favorite_no_circuit_uuid": {
+ "message": "No se pudo derivar un objetivo favorito de la entidad {entity_id}. Seleccione un sensor de circuito (corriente/potencia) o un sensor de subdispositivo."
+ },
+ "favorite_no_device": {
+ "message": "La entidad {entity_id} no está asociada a un dispositivo. Seleccione un sensor de circuito o subdispositivo SPAN."
+ },
+ "favorite_no_unique_id": {
+ "message": "La entidad {entity_id} no tiene un id único y no se puede resolver a un objetivo SPAN."
+ },
+ "favorite_not_span_entity": {
+ "message": "La entidad {entity_id} no es una entidad de SPAN Panel. Seleccione un sensor de circuito, batería (BESS) o cargador EV (EVSE)."
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "El subdispositivo {entity_id} no tiene un dispositivo SPAN Panel principal — su via_device_id no apunta a un panel SPAN."
+ },
"gfe_override_failed": {
"message": "Error al enviar la anulación GFE al panel."
},
+ "graph_horizon_not_available": {
+ "message": "El gestor del horizonte del gráfico no está disponible para el SPAN Panel seleccionado."
+ },
+ "monitoring_not_enabled": {
+ "message": "Ningún SPAN Panel tiene el monitoreo de corriente habilitado."
+ },
"panel_auth_failed": {
"message": "La autenticación con el SPAN Panel falló. Por favor vuelva a autenticarse."
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "Monitoreo habilitado",
"description": "Habilitar o deshabilitar el monitoreo para este circuito."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "Circuito",
"description": "La entidad del sensor de potencia del circuito."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "Monitoreo habilitado",
"description": "Habilitar o deshabilitar el monitoreo para esta fase de alimentación principal."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "Fase de alimentación principal",
"description": "La entidad del sensor de corriente de alimentación principal."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
"get_monitoring_status": {
"name": "Obtener estado de monitoreo",
- "description": "Devuelve el estado actual de monitoreo para todos los circuitos y fases de alimentación principal rastreados."
+ "description": "Devuelve el estado actual de monitoreo para todos los circuitos y fases de alimentación principal rastreados.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
+ }
+ }
},
"set_global_monitoring": {
"name": "Configurar monitoreo global",
"description": "Actualizar los umbrales de monitoreo global y la configuración de notificaciones.",
"fields": {
+ "enabled": {
+ "name": "Habilitado",
+ "description": "Habilitar o deshabilitar el monitoreo de corriente globalmente para este SPAN Panel."
+ },
"continuous_threshold_pct": {
"name": "Umbral continuo",
"description": "Porcentaje de la capacidad del interruptor para la detección de sobrecarga continua."
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "Prioridad de notificación",
"description": "Nivel de prioridad de la notificación push. Controla el comportamiento de interrupción en dispositivos móviles (iOS: interruption-level, Android: priority/canal)."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
"test_notification": {
"name": "Notificación de prueba",
- "description": "Enviar una notificación de prueba a todos los destinos configurados con valores de ejemplo."
+ "description": "Enviar una notificación de prueba a todos los destinos configurados con valores de ejemplo.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
+ }
+ }
},
"set_graph_time_horizon": {
"name": "Establecer horizonte temporal del gráfico",
@@ -532,6 +589,10 @@
"horizon": {
"name": "Horizonte",
"description": "Ventana de tiempo preestablecida para la visualización del gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "Horizonte",
"description": "Ventana de tiempo preestablecida para la visualización del gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "ID del circuito",
"description": "El identificador del circuito para borrar la anulación."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "Horizonte",
"description": "Ventana de tiempo preestablecida para la visualización del gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "ID del subdispositivo",
"description": "El identificador del subdispositivo para borrar la anulación."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
}
}
},
"get_graph_settings": {
"name": "Obtener configuración de gráficos",
- "description": "Devuelve la configuración actual de horizonte de gráficos global y por circuito."
+ "description": "Devuelve la configuración actual de horizonte de gráficos global y por circuito.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuración",
+ "description": "Id de entrada de configuración opcional. Solo necesario cuando hay más de un SPAN Panel configurado."
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "Obtener favoritos",
+ "description": "Devuelve el mapa de favoritos entre paneles, indexado por id de dispositivo del panel SPAN."
+ },
+ "add_favorite": {
+ "name": "Añadir favorito",
+ "description": "Marca un circuito SPAN, batería (BESS) o cargador EV (EVSE) como favorito para que aparezca en la vista de Favoritos del dashboard.",
+ "fields": {
+ "entity_id": {
+ "name": "Entidad",
+ "description": "Cualquier sensor del circuito o subdispositivo a marcar como favorito (corriente, potencia, SoC, etc.)."
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "Eliminar favorito",
+ "description": "Elimina un circuito o subdispositivo de la vista de Favoritos entre paneles.",
+ "fields": {
+ "entity_id": {
+ "name": "Entidad",
+ "description": "Cualquier sensor del circuito o subdispositivo a eliminar de favoritos."
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/translations/fr.json b/custom_components/span_panel/translations/fr.json
index 0ed744d7..712bef06 100644
--- a/custom_components/span_panel/translations/fr.json
+++ b/custom_components/span_panel/translations/fr.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "Aucune entrée de configuration SPAN Panel n'est chargée. Ajoutez et configurez un panneau SPAN avant d'appeler ce service."
},
+ "favorite_no_circuit_uuid": {
+ "message": "Impossible de dériver une cible favorite à partir de l'entité {entity_id}. Sélectionnez un capteur de circuit (courant/puissance) ou un capteur de sous-appareil."
+ },
+ "favorite_no_device": {
+ "message": "L'entité {entity_id} n'est associée à aucun appareil. Sélectionnez un capteur de circuit ou de sous-appareil SPAN."
+ },
+ "favorite_no_unique_id": {
+ "message": "L'entité {entity_id} n'a pas d'identifiant unique et ne peut pas être résolue vers une cible SPAN."
+ },
+ "favorite_not_span_entity": {
+ "message": "L'entité {entity_id} n'est pas une entité SPAN Panel. Sélectionnez un capteur de circuit, de batterie (BESS) ou de chargeur EV (EVSE)."
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "Le sous-appareil {entity_id} n'a pas de panneau SPAN parent — son via_device_id ne pointe pas vers un panneau SPAN."
+ },
"gfe_override_failed": {
"message": "Échec de l'envoi du forçage GFE au panneau."
},
+ "graph_horizon_not_available": {
+ "message": "Le gestionnaire d'horizon de graphique n'est pas disponible pour le panneau SPAN sélectionné."
+ },
+ "monitoring_not_enabled": {
+ "message": "Aucun panneau SPAN n'a la surveillance de courant activée."
+ },
"panel_auth_failed": {
"message": "L'authentification avec le SPAN Panel a échoué. Veuillez vous réauthentifier."
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "Surveillance activée",
"description": "Activer ou désactiver la surveillance pour ce circuit."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "Circuit",
"description": "L'entité capteur de puissance du circuit."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "Surveillance activée",
"description": "Activer ou désactiver la surveillance pour cette phase du réseau."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "Phase du réseau",
"description": "L'entité capteur de courant du réseau principal."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
"get_monitoring_status": {
"name": "Obtenir l'état de surveillance",
- "description": "Retourne l'état actuel de surveillance pour tous les circuits et phases du réseau suivis."
+ "description": "Retourne l'état actuel de surveillance pour tous les circuits et phases du réseau suivis.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
+ }
+ }
},
"set_global_monitoring": {
"name": "Configurer la surveillance globale",
"description": "Mettre à jour les seuils de surveillance globale et les paramètres de notification.",
"fields": {
+ "enabled": {
+ "name": "Activé",
+ "description": "Activer ou désactiver la surveillance du courant globalement pour ce panneau SPAN."
+ },
"continuous_threshold_pct": {
"name": "Seuil continu",
"description": "Pourcentage de la capacité du disjoncteur pour la détection de surcharge continue."
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "Priorité de notification",
"description": "Niveau de priorité de la notification push. Contrôle le comportement d'interruption sur les appareils mobiles (iOS : interruption-level, Android : priority/canal)."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
"test_notification": {
"name": "Notification de test",
- "description": "Envoyer une notification de test à toutes les cibles configurées avec des valeurs d'exemple."
+ "description": "Envoyer une notification de test à toutes les cibles configurées avec des valeurs d'exemple.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
+ }
+ }
},
"set_graph_time_horizon": {
"name": "Définir l'horizon temporel du graphique",
@@ -532,6 +589,10 @@
"horizon": {
"name": "Horizon",
"description": "Fenêtre temporelle prédéfinie pour l'affichage du graphique."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "Horizon",
"description": "Fenêtre temporelle prédéfinie pour l'affichage du graphique."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "ID du circuit",
"description": "L'identifiant du circuit pour lequel effacer le remplacement."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "Horizon",
"description": "Fenêtre temporelle prédéfinie pour l'affichage du graphique."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "ID du sous-appareil",
"description": "L'identifiant du sous-appareil pour lequel effacer le remplacement."
+ },
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
}
}
},
"get_graph_settings": {
"name": "Obtenir les paramètres de graphique",
- "description": "Retourne les paramètres actuels d'horizon de graphique globaux et par circuit."
+ "description": "Retourne les paramètres actuels d'horizon de graphique globaux et par circuit.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrée de configuration",
+ "description": "Id d'entrée de configuration optionnel. Nécessaire uniquement si plusieurs panneaux SPAN sont configurés."
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "Obtenir les favoris",
+ "description": "Retourne la carte des favoris inter-panneaux enregistrés pour les circuits et les sous-appareils, indexée par identifiant d'appareil de panneau SPAN."
+ },
+ "add_favorite": {
+ "name": "Ajouter un favori",
+ "description": "Marque un circuit SPAN, une batterie (BESS) ou un chargeur EV (EVSE) comme favori pour qu'il apparaisse dans la vue Favoris du tableau de bord.",
+ "fields": {
+ "entity_id": {
+ "name": "Entité",
+ "description": "N'importe quel capteur du circuit ou du sous-appareil à marquer comme favori (courant, puissance, SoC, etc.)."
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "Retirer un favori",
+ "description": "Retire un circuit ou un sous-appareil de la vue Favoris inter-panneaux.",
+ "fields": {
+ "entity_id": {
+ "name": "Entité",
+ "description": "N'importe quel capteur du circuit ou du sous-appareil à retirer des favoris."
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/translations/ja.json b/custom_components/span_panel/translations/ja.json
index 2f4bc080..8879fc0e 100644
--- a/custom_components/span_panel/translations/ja.json
+++ b/custom_components/span_panel/translations/ja.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "SPANパネルの設定エントリが読み込まれていません。このサービスを呼び出す前に、SPANパネルを追加して設定してください。"
},
+ "favorite_no_circuit_uuid": {
+ "message": "エンティティ {entity_id} からお気に入り対象を導出できませんでした。回路センサー (電流/電力) またはサブデバイスのセンサーを選択してください。"
+ },
+ "favorite_no_device": {
+ "message": "エンティティ {entity_id} はデバイスに紐付いていません。SPANの回路またはサブデバイスのセンサーを選択してください。"
+ },
+ "favorite_no_unique_id": {
+ "message": "エンティティ {entity_id} は unique_id を持たず、SPANの対象に解決できません。"
+ },
+ "favorite_not_span_entity": {
+ "message": "エンティティ {entity_id} はSPAN Panelのエンティティではありません。回路、バッテリー (BESS)、またはEV充電器 (EVSE) のセンサーを選択してください。"
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "サブデバイス {entity_id} に親のSPAN Panelデバイスがありません。via_device_id がSPANパネルを指していません。"
+ },
"gfe_override_failed": {
"message": "パネルへのGFEオーバーライドの送信に失敗しました。"
},
+ "graph_horizon_not_available": {
+ "message": "選択されたSPANパネルではグラフホライズンマネージャーが利用できません。"
+ },
+ "monitoring_not_enabled": {
+ "message": "電流監視が有効なSPANパネルがありません。"
+ },
"panel_auth_failed": {
"message": "SPAN Panelとの認証に失敗しました。再認証してください。"
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "モニタリング有効",
"description": "この回路のモニタリングを有効または無効にします。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "回路",
"description": "回路電力センサーエンティティ。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "モニタリング有効",
"description": "この主幹レグのモニタリングを有効または無効にします。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "主幹レグ",
"description": "主幹電流センサーエンティティ。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
"get_monitoring_status": {
"name": "モニタリングステータスの取得",
- "description": "すべての追跡対象回路と主幹レグの現在のモニタリング状態を返します。"
+ "description": "すべての追跡対象回路と主幹レグの現在のモニタリング状態を返します。",
+ "fields": {
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
+ }
+ }
},
"set_global_monitoring": {
"name": "グローバル監視の設定",
"description": "グローバル監視のしきい値と通知設定を更新します。",
"fields": {
+ "enabled": {
+ "name": "有効",
+ "description": "このSPANパネルの電流監視をグローバルに有効または無効にします。"
+ },
"continuous_threshold_pct": {
"name": "連続しきい値",
"description": "連続過負荷検出のためのブレーカー定格に対する割合。"
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "通知優先度",
"description": "プッシュ通知の優先度レベル。モバイルデバイスでの割り込み動作を制御します(iOS: interruption-level、Android: priority/チャンネル)。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
"test_notification": {
"name": "テスト通知",
- "description": "サンプル値を使用して設定されたすべてのターゲットにテスト通知を送信します。"
+ "description": "サンプル値を使用して設定されたすべてのターゲットにテスト通知を送信します。",
+ "fields": {
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
+ }
+ }
},
"set_graph_time_horizon": {
"name": "グラフの時間範囲を設定",
@@ -532,6 +589,10 @@
"horizon": {
"name": "時間範囲",
"description": "グラフ表示用の時間ウィンドウプリセット。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "時間範囲",
"description": "グラフ表示用の時間ウィンドウプリセット。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "回路ID",
"description": "オーバーライドをクリアする回路の識別子。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "時間範囲",
"description": "グラフ表示用の時間ウィンドウプリセット。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "サブデバイスID",
"description": "オーバーライドをクリアするサブデバイスの識別子。"
+ },
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
}
}
},
"get_graph_settings": {
"name": "グラフ設定を取得",
- "description": "現在のグローバルおよび回路別のグラフ時間範囲設定を返します。"
+ "description": "現在のグローバルおよび回路別のグラフ時間範囲設定を返します。",
+ "fields": {
+ "config_entry_id": {
+ "name": "設定エントリ",
+ "description": "オプションの設定エントリID。複数のSPANパネルが設定されている場合にのみ必要です。"
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "お気に入りを取得",
+ "description": "SPANパネルのデバイスIDをキーとした、パネル横断のお気に入りマップを返します。"
+ },
+ "add_favorite": {
+ "name": "お気に入りを追加",
+ "description": "SPANの回路、バッテリー (BESS)、またはEV充電器 (EVSE) をお気に入りとしてマークし、ダッシュボードのお気に入りビューに表示します。",
+ "fields": {
+ "entity_id": {
+ "name": "エンティティ",
+ "description": "お気に入りに追加する回路またはサブデバイスの任意のセンサー (電流、電力、SoCなど)。"
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "お気に入りを削除",
+ "description": "パネル横断のお気に入りビューから回路またはサブデバイスを削除します。",
+ "fields": {
+ "entity_id": {
+ "name": "エンティティ",
+ "description": "お気に入りから削除する回路またはサブデバイスの任意のセンサー。"
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/translations/pt.json b/custom_components/span_panel/translations/pt.json
index 417c6a29..3898d7f0 100644
--- a/custom_components/span_panel/translations/pt.json
+++ b/custom_components/span_panel/translations/pt.json
@@ -345,9 +345,30 @@
"export_manifest_no_entries": {
"message": "Nenhuma entrada de configuração do painel SPAN está carregada. Adicione e configure um painel SPAN antes de chamar este serviço."
},
+ "favorite_no_circuit_uuid": {
+ "message": "Não foi possível derivar um destino favorito a partir da entidade {entity_id}. Selecione um sensor de circuito (corrente/potência) ou um sensor de sub-dispositivo."
+ },
+ "favorite_no_device": {
+ "message": "A entidade {entity_id} não está associada a um dispositivo. Selecione um sensor de circuito ou sub-dispositivo SPAN."
+ },
+ "favorite_no_unique_id": {
+ "message": "A entidade {entity_id} não tem um id único e não pode ser resolvida para um destino SPAN."
+ },
+ "favorite_not_span_entity": {
+ "message": "A entidade {entity_id} não é uma entidade do SPAN Panel. Selecione um sensor de circuito, bateria (BESS) ou carregador EV (EVSE)."
+ },
+ "favorite_subdevice_no_span_parent": {
+ "message": "O sub-dispositivo {entity_id} não tem um painel SPAN pai — o seu via_device_id não aponta para um painel SPAN."
+ },
"gfe_override_failed": {
"message": "Falha ao enviar substituição GFE para o painel."
},
+ "graph_horizon_not_available": {
+ "message": "O gerenciador de horizonte do gráfico não está disponível para o painel SPAN selecionado."
+ },
+ "monitoring_not_enabled": {
+ "message": "Nenhum painel SPAN com monitoramento de corrente ativado."
+ },
"panel_auth_failed": {
"message": "A autenticação com o SPAN Panel falhou. Por favor, reautentique."
},
@@ -426,6 +447,10 @@
"monitoring_enabled": {
"name": "Monitorização ativada",
"description": "Ativar ou desativar a monitorização para este circuito."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -436,6 +461,10 @@
"circuit_id": {
"name": "Circuito",
"description": "A entidade do sensor de potência do circuito."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -466,6 +495,10 @@
"monitoring_enabled": {
"name": "Monitorização ativada",
"description": "Ativar ou desativar a monitorização para esta fase da alimentação principal."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -476,17 +509,31 @@
"leg": {
"name": "Fase da alimentação principal",
"description": "A entidade do sensor de corrente da alimentação principal."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
"get_monitoring_status": {
"name": "Obter estado da monitorização",
- "description": "Retorna o estado atual de monitorização para todos os circuitos e fases da alimentação principal monitorizados."
+ "description": "Retorna o estado atual de monitorização para todos os circuitos e fases da alimentação principal monitorizados.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
+ }
+ }
},
"set_global_monitoring": {
"name": "Configurar monitorização global",
"description": "Atualizar os limiares de monitorização global e as definições de notificação.",
"fields": {
+ "enabled": {
+ "name": "Ativado",
+ "description": "Ativar ou desativar o monitoramento de corrente globalmente para este painel SPAN."
+ },
"continuous_threshold_pct": {
"name": "Limiar contínuo",
"description": "Percentagem da capacidade do disjuntor para deteção de sobrecarga contínua."
@@ -518,12 +565,22 @@
"notification_priority": {
"name": "Prioridade de notificação",
"description": "Nível de prioridade da notificação push. Controla o comportamento de interrupção em dispositivos móveis (iOS: interruption-level, Android: priority/canal)."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
"test_notification": {
"name": "Notificação de teste",
- "description": "Enviar uma notificação de teste para todos os destinos configurados com valores de exemplo."
+ "description": "Enviar uma notificação de teste para todos os destinos configurados com valores de exemplo.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
+ }
+ }
},
"set_graph_time_horizon": {
"name": "Definir horizonte temporal do gráfico",
@@ -532,6 +589,10 @@
"horizon": {
"name": "Horizonte",
"description": "Janela temporal predefinida para a exibição do gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -546,6 +607,10 @@
"horizon": {
"name": "Horizonte",
"description": "Janela temporal predefinida para a exibição do gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -556,6 +621,10 @@
"circuit_id": {
"name": "ID do circuito",
"description": "O identificador do circuito para limpar a substituição."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -570,6 +639,10 @@
"horizon": {
"name": "Horizonte",
"description": "Janela temporal predefinida para a exibição do gráfico."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
@@ -580,12 +653,46 @@
"subdevice_id": {
"name": "ID do subdispositivo",
"description": "O identificador do subdispositivo para limpar a substituição."
+ },
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
}
}
},
"get_graph_settings": {
"name": "Obter definições de gráfico",
- "description": "Retorna as definições atuais de horizonte de gráfico globais e por circuito."
+ "description": "Retorna as definições atuais de horizonte de gráfico globais e por circuito.",
+ "fields": {
+ "config_entry_id": {
+ "name": "Entrada de configuração",
+ "description": "Id de entrada de configuração opcional. Necessário apenas quando há mais de um painel SPAN configurado."
+ }
+ }
+ },
+ "get_favorites": {
+ "name": "Obter favoritos",
+ "description": "Retorna o mapa de favoritos entre painéis, indexado pelo id de dispositivo do painel SPAN."
+ },
+ "add_favorite": {
+ "name": "Adicionar favorito",
+ "description": "Marca um circuito SPAN, bateria (BESS) ou carregador EV (EVSE) como favorito para que apareça na vista de Favoritos do painel de controlo.",
+ "fields": {
+ "entity_id": {
+ "name": "Entidade",
+ "description": "Qualquer sensor do circuito ou sub-dispositivo a marcar como favorito (corrente, potência, SoC, etc.)."
+ }
+ }
+ },
+ "remove_favorite": {
+ "name": "Remover favorito",
+ "description": "Remove um circuito ou sub-dispositivo da vista de Favoritos entre painéis.",
+ "fields": {
+ "entity_id": {
+ "name": "Entidade",
+ "description": "Qualquer sensor do circuito ou sub-dispositivo a remover dos favoritos."
+ }
+ }
}
}
}
diff --git a/custom_components/span_panel/websocket.py b/custom_components/span_panel/websocket.py
index 791d6715..aae65dbb 100644
--- a/custom_components/span_panel/websocket.py
+++ b/custom_components/span_panel/websocket.py
@@ -12,6 +12,7 @@
from .const import DOMAIN
from .helpers import build_panel_unique_id
+from .id_builder import build_binary_sensor_unique_id
if TYPE_CHECKING:
from . import SpanPanelRuntimeData
@@ -134,6 +135,10 @@ async def handle_panel_topology(
# Resolve panel-level sensor entity_ids via unique_id registry lookup.
panel_entities = _build_panel_entity_map(snapshot.serial_number, entity_registry)
+ panel_status_entity = _resolve_panel_status_entity(snapshot.serial_number, entity_registry)
+ if panel_status_entity is not None:
+ panel_entities["panel_status"] = panel_status_entity
+
# Single pass over all entities to build circuit_id to role to entity_id
# map. EVSE feed circuit sensors live on the EVSE sub-device, so we
# search all entities for the config entry, not just panel-device ones.
@@ -298,6 +303,20 @@ def _build_panel_entity_map(
return result
+def _resolve_panel_status_entity(
+ serial: str,
+ entity_registry: er.EntityRegistry,
+) -> str | None:
+ """Resolve the panel_status binary sensor entity_id for the frontend.
+
+ The frontend watches this entity to detect panel online/offline state.
+ The binary sensor is always available (see binary_sensor.py) so the
+ frontend can rely on its state regardless of coordinator offline status.
+ """
+ unique_id = build_binary_sensor_unique_id(serial, "panel_status")
+ return entity_registry.async_get_entity_id("binary_sensor", DOMAIN, unique_id)
+
+
def _classify_sensor_role(unique_id: str) -> str | None:
"""Classify a sensor's role from its unique_id suffix."""
for suffix, role in _SENSOR_ROLE_SUFFIXES.items():
diff --git a/frontend.md b/frontend.md
index dc13b00f..25788d71 100644
--- a/frontend.md
+++ b/frontend.md
@@ -29,6 +29,28 @@ Use the **Enable Switches** toggle in the banner to globally enable or disable c

+## List Views — By Activity and By Area
+
+Beyond the panel grid, each panel (and the cross-panel Favorites view) exposes two list-oriented tabs:
+
+- **By Activity** — circuits sorted by live power (or current, when the unit toggle is on Amps), highest first. A search box filters rows by name.
+- **By Area** — circuits grouped by their Home Assistant area (entity-level assignment first, then device-level fallback). Unassigned circuits land in a
+ dedicated group at the bottom.
+
+Each row shows breaker rating, live utilization %, circuit name, shedding-priority icon, an ON/OFF toggle (tappable once the **Enable Switches** slider is
+armed), live power / current value, a gear icon that opens the side panel, and a chevron to expand the row's chart.
+
+### List view columns
+
+By default the list views stack circuits one per row. You can switch to a **2** or **3** column grid from the **Graph Settings** side panel (gear icon at the
+top of the panel header) → **List View Columns**.
+
+- Rows flow left-to-right, top-to-bottom, so the existing sort order (value-desc for By Activity, alphabetical-within-area for By Area) becomes top-left through
+ bottom-right.
+- Expanding a row shows its chart directly below, constrained to the same column as the row.
+- Viewports narrower than 600 px force single-column regardless of the setting.
+- The choice is stored per browser in `localStorage`, so you can keep a phone in single-column and a desktop in three columns.
+
## Monitoring View
The Monitoring tab provides current-based alerting for individual circuits. It detects sustained high utilization and transient spikes relative to each
@@ -71,6 +93,53 @@ values.

-## Settings View
+## Favorites View
+
+The dashboard supports a cross-panel **Favorites** view that lets you curate a single workspace from circuits and sub-devices (BESS, EVSE) belonging to any of
+your configured SPAN panels. This is useful when you want a single place to keep an eye on a small set of important loads — say, the EV charger on Panel A and
+the heat pump on Panel B — without switching panels in the dropdown.
+
+### Marking favorites
+
+Favorites are marked from the **side panels** that open via the gear icons in the dashboard. The standalone span-card (used in Lovelace dashboards) does not
+expose hearts because it has no Favorites view.
+
+There are three places to toggle a favorite:
+
+- **Panel-level "Graph Settings" side panel** — opened from the gear icon at the top of the panel header. The per-circuit and per-sub-device lists each render a
+ heart icon next to the time-horizon dropdown. Click the heart to favorite or un-favorite that target without leaving the list.
+- **Per-circuit side panel** — opened from a circuit's gear icon in the breaker grid (or in the By Activity / By Area rows). A "Favorite" section with a switch
+ sits between the relay control and the shedding priority.
+- **Per-sub-device side panel** — opened from the gear icon on a BESS or EVSE tile. Same Favorite section as the per-circuit panel.
+
+A circuit or sub-device can be favorited on any panel. The integration stores the favorites under the configured SPAN integration's storage, so they sync across
+browsers and devices.
+
+### The Favorites entry
+
+When you have at least one favorite configured, a synthetic **Favorites** entry appears at the top of the panel dropdown. Switching to it loads the aggregated
+view. Removing the last favorite while the Favorites view is active automatically switches the dropdown and view back to the first real panel.
+
+### Tabs in the Favorites view
+
+The Favorites view exposes **By Activity**, **By Area**, and **Monitoring** tabs. **By Panel** is not available because the physical breaker grid is inherently
+single-panel.
+
+- **By Activity / By Area** — Show the favorited circuits sorted by power (or grouped by area). Sub-device tiles render above the circuit list. When more than
+ one panel contributes favorites, circuit and sub-device names are prefixed with their panel name so you can tell them apart.
+- **Monitoring** — Stacks one Monitoring block per contributing panel, since threshold and notification settings are per-panel.
+
+The Favorites view is **stateful**: the active tab, expanded rows, and search query are remembered in the browser and restored when you come back to it.
+
+### Editing from the Favorites view
+
+Opening a circuit's or sub-device's gear from the Favorites view targets the originating panel for any side-panel edits (graph horizon, monitoring thresholds,
+relay state, etc.). Settings always go to the right panel even though the targets are aggregated in the Favorites view.
+
+### Services
+
+The favorites map is also exposed as services for automations and scripts:
-The Settings tab provides access to integration configuration options without navigating through the Home Assistant settings menu.
+- `span_panel.get_favorites` — Returns the current map.
+- `span_panel.add_favorite` / `span_panel.remove_favorite` — Take a single `entity_id` (any sensor on the circuit or sub-device — current, power, SoC, etc.).
+ The integration resolves the entity to its panel and target, so you never need to know internal circuit UUIDs or HA device IDs.
diff --git a/pyproject.toml b/pyproject.toml
index 3246440f..e7ed7736 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "span"
-version = "2.0.5"
+version = "2.0.6"
description = "Span Panel Custom Integration for Home Assistant"
authors = [{name = "SpanPanel"}]
license = {text = "MIT"}
@@ -8,7 +8,7 @@ readme = "README.md"
requires-python = ">=3.14.2,<3.15"
dependencies = [
"homeassistant==2026.4.2",
- "span-panel-api==2.5.4",
+ "span-panel-api==2.6.1",
]
[dependency-groups]
diff --git a/tests/test_coordinator.py b/tests/test_coordinator.py
index 8ed1a3fa..48427766 100644
--- a/tests/test_coordinator.py
+++ b/tests/test_coordinator.py
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
+from typing import cast
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
@@ -16,6 +17,7 @@
from custom_components.span_panel.coordinator import SpanPanelCoordinator
from homeassistant.core import HomeAssistant
+from span_panel_api import SpanMqttClient
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
@@ -34,13 +36,13 @@
def _create_coordinator(
hass: HomeAssistant,
*,
- client: MagicMock | None = None,
+ client: object | None = None,
options: dict | None = None,
) -> SpanPanelCoordinator:
"""Create a coordinator with mocked dependencies."""
return SpanPanelCoordinator(
hass,
- client or MagicMock(),
+ cast(SpanMqttClient, client or MagicMock()),
MockConfigEntry(
domain="span_panel",
options=options or {},
@@ -187,8 +189,10 @@ async def test_fire_dip_notification_noops_without_events(hass: HomeAssistant) -
async def test_async_setup_streaming_registers_callback_and_starts_client(
hass: HomeAssistant,
) -> None:
- """Streaming setup should register the callback and start the client."""
+ """Streaming setup should register both callbacks and start the client."""
client = MagicMock()
+ unregister_connection = MagicMock()
+ client.register_connection_callback = MagicMock(return_value=unregister_connection)
client.register_snapshot_callback = MagicMock(return_value=MagicMock())
client.start_streaming = AsyncMock()
coordinator = _create_coordinator(hass, client=client)
@@ -198,6 +202,8 @@ async def test_async_setup_streaming_registers_callback_and_starts_client(
client.register_snapshot_callback.assert_called_once()
client.start_streaming.assert_awaited_once()
assert coordinator._unregister_streaming is not None
+ client.register_connection_callback.assert_called_once_with(coordinator._on_connection_change)
+ assert coordinator._unregister_connection is not None
async def test_on_snapshot_push_updates_state_and_runs_post_tasks(
@@ -386,3 +392,118 @@ async def test_async_reload_task_handles_expected_errors(
await coordinator._async_reload_task()
assert "Home Assistant error during reload: reload failed" in caplog.text
+
+
+async def test_connection_callback_registered_and_unregistered_on_lifecycle(
+ hass: HomeAssistant,
+) -> None:
+ """async_setup_streaming should register a connection callback; async_shutdown should unregister it."""
+ client = MagicMock()
+ client.register_connection_callback = MagicMock()
+ client.register_snapshot_callback = MagicMock()
+ client.start_streaming = AsyncMock()
+ client.stop_streaming = AsyncMock()
+ client.close = AsyncMock()
+
+ # register_connection_callback returns an unregister function
+ unregister_connection = MagicMock()
+ client.register_connection_callback.return_value = unregister_connection
+ client.register_snapshot_callback.return_value = MagicMock()
+
+ coordinator = _create_coordinator(hass, client=client)
+
+ await coordinator.async_setup_streaming()
+
+ # Connection callback was registered exactly once with the coordinator's handler
+ client.register_connection_callback.assert_called_once_with(coordinator._on_connection_change)
+ assert coordinator._unregister_connection is unregister_connection
+
+ await coordinator.async_shutdown()
+
+ # Unregister was invoked and the field cleared
+ cast(MagicMock, unregister_connection).assert_called_once_with()
+ assert coordinator._unregister_connection is None
+
+
+async def test_on_connection_change_false_flips_offline_and_notifies_listeners(
+ hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+ """A False edge must flip panel_offline True, log once, and push a listener update."""
+ coordinator = _create_coordinator(hass)
+ assert coordinator.panel_offline is False
+
+ with patch.object(coordinator, "async_update_listeners") as notify:
+ with caplog.at_level(logging.INFO):
+ coordinator._on_connection_change(False)
+
+ assert coordinator.panel_offline is True
+ notify.assert_called_once_with()
+ assert any(
+ "is unavailable" in r.message and "MQTT broker disconnected" in r.message
+ for r in caplog.records
+ )
+
+
+async def test_on_connection_change_true_clears_offline_and_notifies_listeners(
+ hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+ """A True edge must flip panel_offline False, log once, and push a listener update."""
+ coordinator = _create_coordinator(hass)
+ coordinator._panel_offline = True
+
+ with patch.object(coordinator, "async_update_listeners") as notify:
+ with caplog.at_level(logging.INFO):
+ coordinator._on_connection_change(True)
+
+ assert coordinator.panel_offline is False
+ notify.assert_called_once_with()
+ assert any("is back online" in r.message for r in caplog.records)
+
+
+async def test_on_connection_change_noop_when_state_unchanged(
+ hass: HomeAssistant,
+) -> None:
+ """When connected state matches current panel_offline, no listener fan-out."""
+ coordinator = _create_coordinator(hass)
+
+ # Already online (panel_offline=False); receiving another True edge is a no-op
+ with patch.object(coordinator, "async_update_listeners") as notify_online_case:
+ coordinator._on_connection_change(True)
+ notify_online_case.assert_not_called()
+ assert coordinator.panel_offline is False
+
+ # Already offline; receiving another False edge is a no-op
+ coordinator._panel_offline = True
+ with patch.object(coordinator, "async_update_listeners") as notify_offline_case:
+ coordinator._on_connection_change(False)
+ notify_offline_case.assert_not_called()
+ assert coordinator.panel_offline is True
+
+
+async def test_async_update_data_stale_data_error_marks_offline_and_returns_last_data(
+ hass: HomeAssistant, caplog: pytest.LogCaptureFixture
+) -> None:
+ """A SpanPanelStaleDataError from get_snapshot() should be treated as an expected offline signal."""
+ from span_panel_api.exceptions import SpanPanelStaleDataError
+
+ last_snapshot = SpanPanelSnapshotFactory.create()
+
+ client = MagicMock()
+ client.get_snapshot = AsyncMock(
+ side_effect=SpanPanelStaleDataError("MQTT broker disconnected")
+ )
+
+ coordinator = _create_coordinator(hass, client=client)
+ # Simulate a prior successful update
+ coordinator.data = last_snapshot
+ assert coordinator.panel_offline is False
+
+ with caplog.at_level(logging.INFO):
+ result = await coordinator._async_update_data()
+
+ assert result is last_snapshot
+ assert coordinator.panel_offline is True
+ assert any(
+ "is unavailable" in r.message and "MQTT broker disconnected" in r.message
+ for r in caplog.records
+ )
diff --git a/tests/test_favorites_service.py b/tests/test_favorites_service.py
new file mode 100644
index 00000000..ab596ac3
--- /dev/null
+++ b/tests/test_favorites_service.py
@@ -0,0 +1,597 @@
+"""Tests for cross-panel favorites storage helpers and services."""
+
+from __future__ import annotations
+
+from typing import Any
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from homeassistant.core import SupportsResponse
+from homeassistant.exceptions import ServiceValidationError
+
+from custom_components.span_panel.const import DOMAIN
+from custom_components.span_panel.frontend import (
+ async_get_favorites,
+ async_set_favorite,
+)
+from custom_components.span_panel.services import _async_register_favorites_services
+
+
+class _FakeStore:
+ """In-memory stand-in for homeassistant.helpers.storage.Store.
+
+ One shared backing dict keyed by storage key, so multiple Store(...) calls
+ in the same test see a consistent view of the data.
+ """
+
+ _shared_state: dict[str, Any] = {}
+
+ def __init__(self, _hass: Any, _version: int, key: str) -> None:
+ self._key = key
+
+ async def async_load(self) -> Any:
+ return _FakeStore._shared_state.get(self._key)
+
+ async def async_save(self, data: Any) -> None:
+ _FakeStore._shared_state[self._key] = data
+
+ @classmethod
+ def reset(cls) -> None:
+ cls._shared_state = {}
+
+ @classmethod
+ def preload(cls, key: str, data: Any) -> None:
+ cls._shared_state[key] = data
+
+
+@pytest.fixture(autouse=True)
+def _reset_store() -> None:
+ _FakeStore.reset()
+
+
+@pytest.fixture
+def _patched_store() -> Any:
+ with patch(
+ "custom_components.span_panel.frontend.Store",
+ _FakeStore,
+ ):
+ yield
+
+
+def _panel_entry(circuits: list[str] | None = None, sub_devices: list[str] | None = None) -> dict[str, list[str]]:
+ return {"circuits": circuits or [], "sub_devices": sub_devices or []}
+
+
+class TestAsyncGetFavorites:
+ """Tests for ``async_get_favorites`` helper."""
+
+ @pytest.mark.asyncio
+ async def test_empty_storage_returns_empty_dict(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ result = await async_get_favorites(hass)
+ assert result == {}
+
+ @pytest.mark.asyncio
+ async def test_returns_stored_favorites_new_shape(self, _patched_store: Any) -> None:
+ _FakeStore.preload(
+ "span_panel_settings",
+ {
+ "show_panel": True,
+ "favorites": {
+ "panel_a": {"circuits": ["c1", "c2"], "sub_devices": ["bess1"]},
+ "panel_b": {"circuits": ["c3"], "sub_devices": []},
+ },
+ },
+ )
+ hass = MagicMock()
+ result = await async_get_favorites(hass)
+ assert result == {
+ "panel_a": _panel_entry(["c1", "c2"], ["bess1"]),
+ "panel_b": _panel_entry(["c3"], []),
+ }
+
+ @pytest.mark.asyncio
+ async def test_legacy_list_shape_is_circuits_only(self, _patched_store: Any) -> None:
+ """Pre-existing favorites stored as flat lists migrate to circuits-only entries."""
+ _FakeStore.preload(
+ "span_panel_settings",
+ {"favorites": {"panel_a": ["c1", "c2"], "panel_b": ["c3"]}},
+ )
+ hass = MagicMock()
+ result = await async_get_favorites(hass)
+ assert result == {
+ "panel_a": _panel_entry(["c1", "c2"]),
+ "panel_b": _panel_entry(["c3"]),
+ }
+
+ @pytest.mark.asyncio
+ async def test_filters_invalid_shapes(self, _patched_store: Any) -> None:
+ """Malformed entries (non-str values, missing kinds, empties) are dropped."""
+ _FakeStore.preload(
+ "span_panel_settings",
+ {
+ "favorites": {
+ "panel_a": {"circuits": ["c1", "", 42, "c2"], "sub_devices": "bad"},
+ "panel_b": "not-a-dict",
+ 123: {"circuits": ["c3"]}, # type: ignore[dict-item]
+ "panel_empty": {"circuits": [], "sub_devices": []},
+ }
+ },
+ )
+ hass = MagicMock()
+ result = await async_get_favorites(hass)
+ assert result == {"panel_a": _panel_entry(["c1", "c2"], [])}
+
+ @pytest.mark.asyncio
+ async def test_tolerates_missing_favorites_key(self, _patched_store: Any) -> None:
+ _FakeStore.preload(
+ "span_panel_settings", {"show_panel": False, "panel_admin_only": True}
+ )
+ hass = MagicMock()
+ result = await async_get_favorites(hass)
+ assert result == {}
+
+
+class TestAsyncSetFavorite:
+ """Tests for ``async_set_favorite`` helper."""
+
+ @pytest.mark.asyncio
+ async def test_add_circuit_creates_panel_entry(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ result = await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ assert result == {"panel_a": _panel_entry(["c1"])}
+ persisted = _FakeStore._shared_state["span_panel_settings"]
+ assert persisted["favorites"] == {"panel_a": _panel_entry(["c1"])}
+
+ @pytest.mark.asyncio
+ async def test_add_subdevice_coexists_with_circuits(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ result = await async_set_favorite(hass, "panel_a", "sub_devices", "bess1", True)
+ assert result == {"panel_a": _panel_entry(["c1"], ["bess1"])}
+
+ @pytest.mark.asyncio
+ async def test_add_dedupes(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ result = await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ assert result == {"panel_a": _panel_entry(["c1"])}
+
+ @pytest.mark.asyncio
+ async def test_remove_drops_empty_panel_key(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ result = await async_set_favorite(hass, "panel_a", "circuits", "c1", False)
+ assert result == {}
+ persisted = _FakeStore._shared_state["span_panel_settings"]
+ assert persisted["favorites"] == {}
+
+ @pytest.mark.asyncio
+ async def test_remove_keeps_other_kind_entries(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ await async_set_favorite(hass, "panel_a", "sub_devices", "bess1", True)
+ result = await async_set_favorite(hass, "panel_a", "circuits", "c1", False)
+ assert result == {"panel_a": _panel_entry([], ["bess1"])}
+
+ @pytest.mark.asyncio
+ async def test_remove_of_unknown_is_noop(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ result = await async_set_favorite(hass, "panel_a", "circuits", "missing", False)
+ assert result == {}
+
+ @pytest.mark.asyncio
+ async def test_preserves_sibling_settings(self, _patched_store: Any) -> None:
+ """Favorites writes must not trample ``show_panel`` or ``panel_admin_only``."""
+ _FakeStore.preload(
+ "span_panel_settings", {"show_panel": False, "panel_admin_only": True}
+ )
+ hass = MagicMock()
+ await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ persisted = _FakeStore._shared_state["span_panel_settings"]
+ assert persisted["show_panel"] is False
+ assert persisted["panel_admin_only"] is True
+ assert persisted["favorites"] == {"panel_a": _panel_entry(["c1"])}
+
+ @pytest.mark.asyncio
+ async def test_unknown_kind_raises(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ with pytest.raises(ValueError):
+ await async_set_favorite(hass, "panel_a", "bogus", "c1", True)
+
+ @pytest.mark.asyncio
+ async def test_set_migrates_legacy_shape_in_place(self, _patched_store: Any) -> None:
+ """Touching a panel with legacy ``[uuid]`` storage rewrites it as the canonical dict."""
+ _FakeStore.preload(
+ "span_panel_settings",
+ {"favorites": {"panel_a": ["c1", "c2"]}},
+ )
+ hass = MagicMock()
+ # No new circuit added; the same uuid we already had.
+ result = await async_set_favorite(hass, "panel_a", "circuits", "c1", True)
+ assert result == {"panel_a": _panel_entry(["c1", "c2"])}
+ persisted = _FakeStore._shared_state["span_panel_settings"]["favorites"]
+ # Persisted shape is now the nested dict, not the legacy list.
+ assert persisted == {"panel_a": _panel_entry(["c1", "c2"])}
+
+ @pytest.mark.asyncio
+ async def test_concurrent_set_favorites_does_not_drop_writes(
+ self, _patched_store: Any
+ ) -> None:
+ """Two parallel adds must both end up in storage (lock prevents lost writes)."""
+ import asyncio
+
+ hass = MagicMock()
+ # Run both adds concurrently; each call goes through the lock so the
+ # second write picks up the first's mutation.
+ results = await asyncio.gather(
+ async_set_favorite(hass, "panel_a", "circuits", "c1", True),
+ async_set_favorite(hass, "panel_a", "circuits", "c2", True),
+ )
+ # Both calls return the favorites map at the time they wrote;
+ # the FINAL persisted state must contain both circuits.
+ persisted = _FakeStore._shared_state["span_panel_settings"]["favorites"]
+ assert sorted(persisted["panel_a"]["circuits"]) == ["c1", "c2"]
+ # Each call's returned map is at least non-empty.
+ for r in results:
+ assert "panel_a" in r
+
+
+def _capture_registered_handlers(hass: MagicMock) -> dict[str, Any]:
+ """Run ``_async_register_favorites_services`` and return a name->handler map."""
+ handlers: dict[str, Any] = {}
+ schemas: dict[str, Any] = {}
+ responses: dict[str, Any] = {}
+
+ def _register(
+ domain: str,
+ service: str,
+ handler: Any,
+ schema: Any | None = None,
+ supports_response: Any = SupportsResponse.NONE,
+ ) -> None:
+ assert domain == DOMAIN
+ handlers[service] = handler
+ schemas[service] = schema
+ responses[service] = supports_response
+
+ hass.services = MagicMock()
+ hass.services.async_register = MagicMock(side_effect=_register)
+ _async_register_favorites_services(hass)
+ return {"handlers": handlers, "schemas": schemas, "responses": responses}
+
+
+def _make_service_call(data: dict[str, Any]) -> MagicMock:
+ call = MagicMock()
+ call.data = data
+ return call
+
+
+def _make_entity_entry(
+ *,
+ platform: str = DOMAIN,
+ unique_id: str = "span_sp3-242424_abcdef0123456789abcdef0123456789_power",
+ device_id: str | None = "d_main",
+) -> MagicMock:
+ entry = MagicMock()
+ entry.platform = platform
+ entry.unique_id = unique_id
+ entry.device_id = device_id
+ return entry
+
+
+def _make_device_entry(
+ *,
+ device_id: str = "d_main",
+ identifiers: set[tuple[str, str]] | None = None,
+ via_device_id: str | None = None,
+) -> MagicMock:
+ device = MagicMock()
+ device.id = device_id
+ device.identifiers = identifiers if identifiers is not None else {(DOMAIN, "serial_a")}
+ device.via_device_id = via_device_id
+ return device
+
+
+def _patch_registries(entity: MagicMock | None, device: MagicMock | None) -> Any:
+ """Patch services.er.async_get and services.dr.async_get for a single call.
+
+ Either registry returns the given object from ``async_get`` regardless of id.
+ """
+ entity_reg = MagicMock()
+ entity_reg.async_get = MagicMock(return_value=entity)
+ device_reg = MagicMock()
+ device_reg.async_get = MagicMock(return_value=device)
+
+ return patch.multiple(
+ "custom_components.span_panel.services",
+ er=MagicMock(async_get=MagicMock(return_value=entity_reg)),
+ dr=MagicMock(async_get=MagicMock(return_value=device_reg)),
+ )
+
+
+def _patch_registries_for_subdevice(
+ entity: MagicMock,
+ sub_device: MagicMock,
+ parent_panel: MagicMock,
+) -> Any:
+ """Stub registries so device_registry.async_get returns sub_device for the
+ entity's device_id and parent_panel for the via_device_id lookup."""
+ entity_reg = MagicMock()
+ entity_reg.async_get = MagicMock(return_value=entity)
+
+ device_reg = MagicMock()
+ def _device_lookup(device_id: str) -> MagicMock | None:
+ if device_id == sub_device.id:
+ return sub_device
+ if device_id == parent_panel.id:
+ return parent_panel
+ return None
+ device_reg.async_get = MagicMock(side_effect=_device_lookup)
+
+ return patch.multiple(
+ "custom_components.span_panel.services",
+ er=MagicMock(async_get=MagicMock(return_value=entity_reg)),
+ dr=MagicMock(async_get=MagicMock(return_value=device_reg)),
+ )
+
+
+class TestFavoritesServiceHandlers:
+ """Tests for the ``get_favorites`` / ``add_favorite`` / ``remove_favorite`` service handlers."""
+
+ @pytest.mark.asyncio
+ async def test_get_favorites_returns_current_map(self, _patched_store: Any) -> None:
+ _FakeStore.preload(
+ "span_panel_settings",
+ {"favorites": {"panel_a": {"circuits": ["c1"], "sub_devices": []}}},
+ )
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["get_favorites"]
+
+ result = await handler(_make_service_call({}))
+ assert result == {
+ "favorites": {"panel_a": _panel_entry(["c1"])},
+ }
+ assert registered["responses"]["get_favorites"] is SupportsResponse.ONLY
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_unknown_entity(
+ self, _patched_store: Any
+ ) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ with _patch_registries(entity=None, device=None):
+ with pytest.raises(ServiceValidationError):
+ await handler(_make_service_call({"entity_id": "sensor.unknown"}))
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_non_span_platform(
+ self, _patched_store: Any
+ ) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ foreign_entity = _make_entity_entry(platform="other_domain")
+ with _patch_registries(entity=foreign_entity, device=None):
+ with pytest.raises(ServiceValidationError):
+ await handler(_make_service_call({"entity_id": "sensor.other_power"}))
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_accepts_sub_device_entity(
+ self, _patched_store: Any
+ ) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ entity = _make_entity_entry(
+ unique_id="span_sp3-242424_storage_battery_percentage",
+ device_id="d_bess",
+ )
+ sub_device = _make_device_entry(
+ device_id="d_bess",
+ identifiers={(DOMAIN, "serial_a_bess")},
+ via_device_id="d_main",
+ )
+ parent = _make_device_entry(
+ device_id="d_main",
+ identifiers={(DOMAIN, "serial_a")},
+ via_device_id=None,
+ )
+
+ with _patch_registries_for_subdevice(entity, sub_device, parent):
+ result = await handler(
+ _make_service_call({"entity_id": "sensor.battery_level"})
+ )
+
+ assert result == {"favorites": {"d_main": _panel_entry([], ["d_bess"])}}
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_on_evse_feed_circuit_sensor_favorites_circuit(
+ self, _patched_store: Any
+ ) -> None:
+ """EVSE feed-circuit sensors are re-assigned to the EVSE sub-device via
+ a device override, but their unique_id still carries the circuit UUID.
+ Favoriting such an entity must store a *circuit* favorite keyed by the
+ parent panel, not a sub-device favorite keyed by the EVSE."""
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ circuit_uuid = "abcdef0123456789abcdef0123456789"
+ entity = _make_entity_entry(
+ unique_id=f"span_sp3-242424_{circuit_uuid}_power",
+ device_id="d_evse",
+ )
+ evse_sub_device = _make_device_entry(
+ device_id="d_evse",
+ identifiers={(DOMAIN, "serial_a_evse")},
+ via_device_id="d_main",
+ )
+ parent = _make_device_entry(
+ device_id="d_main",
+ identifiers={(DOMAIN, "serial_a")},
+ via_device_id=None,
+ )
+
+ with _patch_registries_for_subdevice(entity, evse_sub_device, parent):
+ result = await handler(
+ _make_service_call({"entity_id": "sensor.evse_feed_power"})
+ )
+
+ assert result == {"favorites": {"d_main": _panel_entry([circuit_uuid])}}
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_entity_without_uuid_in_unique_id(
+ self, _patched_store: Any
+ ) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ # Panel-level sensor (no circuit uuid segment in unique_id).
+ entity = _make_entity_entry(unique_id="span_sp3-242424_instantGridPowerW")
+ device = _make_device_entry()
+
+ with _patch_registries(entity=entity, device=device):
+ with pytest.raises(ServiceValidationError):
+ await handler(_make_service_call({"entity_id": "sensor.panel_power"}))
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_persists_and_returns_map(
+ self, _patched_store: Any
+ ) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ circuit_uuid = "abcdef0123456789abcdef0123456789"
+ entity = _make_entity_entry(
+ unique_id=f"span_sp3-242424_{circuit_uuid}_power",
+ device_id="d_main",
+ )
+ device = _make_device_entry(device_id="d_main")
+
+ with _patch_registries(entity=entity, device=device):
+ result = await handler(
+ _make_service_call({"entity_id": "sensor.kitchen_power"})
+ )
+
+ assert result == {"favorites": {"d_main": _panel_entry([circuit_uuid])}}
+ assert _FakeStore._shared_state["span_panel_settings"]["favorites"] == {
+ "d_main": _panel_entry([circuit_uuid])
+ }
+
+ @pytest.mark.asyncio
+ async def test_remove_favorite_resolves_via_entity_registry(
+ self, _patched_store: Any
+ ) -> None:
+ circuit_uuid = "abcdef0123456789abcdef0123456789"
+ _FakeStore.preload(
+ "span_panel_settings",
+ {"favorites": {"d_main": _panel_entry([circuit_uuid])}},
+ )
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["remove_favorite"]
+
+ entity = _make_entity_entry(
+ unique_id=f"span_sp3-242424_{circuit_uuid}_power",
+ device_id="d_main",
+ )
+ device = _make_device_entry(device_id="d_main")
+
+ with _patch_registries(entity=entity, device=device):
+ result = await handler(
+ _make_service_call({"entity_id": "sensor.kitchen_power"})
+ )
+
+ assert result == {"favorites": {}}
+
+ def test_mutation_responses_are_optional(self, _patched_store: Any) -> None:
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ assert registered["responses"]["add_favorite"] is SupportsResponse.OPTIONAL
+ assert registered["responses"]["remove_favorite"] is SupportsResponse.OPTIONAL
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_entity_with_no_device_id(
+ self, _patched_store: Any
+ ) -> None:
+ """Entities with no ``device_id`` are not favoritable."""
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ orphan = _make_entity_entry(device_id=None)
+ with _patch_registries(entity=orphan, device=None):
+ with pytest.raises(ServiceValidationError):
+ await handler(_make_service_call({"entity_id": "sensor.orphan"}))
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_subdevice_with_non_span_parent(
+ self, _patched_store: Any
+ ) -> None:
+ """Sub-device whose ``via_device_id`` does NOT point at a SPAN panel."""
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ entity = _make_entity_entry(device_id="d_sub")
+ sub_device = _make_device_entry(
+ device_id="d_sub",
+ identifiers={(DOMAIN, "serial_a_bess")},
+ via_device_id="d_foreign",
+ )
+ # Parent exists but isn't a SPAN device (different domain identifier).
+ foreign_parent = _make_device_entry(
+ device_id="d_foreign",
+ identifiers={("other_domain", "xyz")},
+ via_device_id=None,
+ )
+
+ with _patch_registries_for_subdevice(entity, sub_device, foreign_parent):
+ with pytest.raises(ServiceValidationError):
+ await handler(
+ _make_service_call({"entity_id": "sensor.bess_under_foreign"})
+ )
+
+ @pytest.mark.asyncio
+ async def test_add_favorite_rejects_subdevice_with_missing_parent(
+ self, _patched_store: Any
+ ) -> None:
+ """Sub-device whose ``via_device_id`` references a missing device."""
+ hass = MagicMock()
+ registered = _capture_registered_handlers(hass)
+ handler = registered["handlers"]["add_favorite"]
+
+ entity = _make_entity_entry(device_id="d_sub")
+ sub_device = _make_device_entry(
+ device_id="d_sub",
+ identifiers={(DOMAIN, "serial_a_bess")},
+ via_device_id="d_missing",
+ )
+
+ # Stub a registry where the parent lookup returns None.
+ entity_reg = MagicMock()
+ entity_reg.async_get = MagicMock(return_value=entity)
+
+ device_reg = MagicMock()
+ def _device_lookup(device_id: str) -> MagicMock | None:
+ return sub_device if device_id == "d_sub" else None
+ device_reg.async_get = MagicMock(side_effect=_device_lookup)
+
+ with patch.multiple(
+ "custom_components.span_panel.services",
+ er=MagicMock(async_get=MagicMock(return_value=entity_reg)),
+ dr=MagicMock(async_get=MagicMock(return_value=device_reg)),
+ ):
+ with pytest.raises(ServiceValidationError):
+ await handler(
+ _make_service_call({"entity_id": "sensor.bess_orphaned"})
+ )
diff --git a/tests/test_websocket.py b/tests/test_websocket.py
index 8b0c984c..ea45eb5a 100644
--- a/tests/test_websocket.py
+++ b/tests/test_websocket.py
@@ -745,6 +745,76 @@ async def test_topology_includes_always_on_and_priority(self, hass: HomeAssistan
assert hvac_data["priority"] == "SOC_THRESHOLD"
@pytest.mark.asyncio
+ async def test_topology_includes_panel_status_entity(self, hass: HomeAssistant):
+ """panel_status binary sensor entity_id is included in the topology panel_entities map."""
+ snapshot = SpanPanelSnapshotFactory.create(serial_number="sp3-242424-001")
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={},
+ entry_id="span_entry",
+ unique_id="sp3-242424-001",
+ )
+ entry.add_to_hass(hass)
+ entry.mock_state(hass, ConfigEntryState.LOADED)
+ entry.runtime_data = SpanPanelRuntimeData(
+ coordinator=_make_coordinator(snapshot)
+ )
+
+ panel_device = _register_panel_device(hass, "span_entry", serial="sp3-242424-001")
+
+ _register_entity(
+ hass,
+ "span_entry",
+ panel_device.id,
+ "binary_sensor",
+ "span_sp3-242424-001_panel_status",
+ "binary_sensor.span_panel_test_panel_status",
+ )
+
+ connection = _make_mock_connection()
+ msg = {"id": 1, "type": "span_panel/panel_topology", "device_id": panel_device.id}
+
+ await _handle_panel_topology_inner(hass, connection, msg)
+
+ connection.send_error.assert_not_called()
+ connection.send_result.assert_called_once()
+
+ result = connection.send_result.call_args[0][1]
+ assert result["panel_entities"]["panel_status"] == "binary_sensor.span_panel_test_panel_status"
+
+ @pytest.mark.asyncio
+ async def test_topology_omits_panel_status_when_entity_missing(self, hass: HomeAssistant):
+ """panel_status is absent from the topology map when no binary_sensor entity exists."""
+ snapshot = SpanPanelSnapshotFactory.create(serial_number="sp3-242424-001")
+
+ entry = MockConfigEntry(
+ domain=DOMAIN,
+ data={},
+ entry_id="span_entry",
+ unique_id="sp3-242424-001",
+ )
+ entry.add_to_hass(hass)
+ entry.mock_state(hass, ConfigEntryState.LOADED)
+ entry.runtime_data = SpanPanelRuntimeData(
+ coordinator=_make_coordinator(snapshot)
+ )
+
+ panel_device = _register_panel_device(hass, "span_entry", serial="sp3-242424-001")
+
+ # Intentionally do NOT register a binary_sensor.*_panel_status entity
+
+ connection = _make_mock_connection()
+ msg = {"id": 1, "type": "span_panel/panel_topology", "device_id": panel_device.id}
+
+ await _handle_panel_topology_inner(hass, connection, msg)
+
+ connection.send_error.assert_not_called()
+ connection.send_result.assert_called_once()
+
+ result = connection.send_result.call_args[0][1]
+ assert "panel_status" not in result["panel_entities"]
+
@pytest.mark.asyncio
async def test_registration(self, hass: HomeAssistant):
"""WebSocket commands can be registered without error."""
diff --git a/uv.lock b/uv.lock
index c2e14e94..55e72965 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2352,7 +2352,7 @@ wheels = [
[[package]]
name = "span"
-version = "2.0.5"
+version = "2.0.6"
source = { virtual = "." }
dependencies = [
{ name = "homeassistant" },
@@ -2407,7 +2407,7 @@ dev = [
[[package]]
name = "span-panel-api"
-version = "2.5.4"
+version = "2.6.1"
source = { editable = "../span-panel-api" }
dependencies = [
{ name = "httpx" },