// Enhanced Power Flow Card â€" Visual Editor (Vanilla Web Component) // Avoids importing 'lit' to ensure it works in all HA environments const CARD_VERSION = '3.0.5'; const FLOW_COLOR_DEFAULTS = { ac_input: { positive: '#2f80ed', negative: '#f2994a' }, ac_output: { positive: '#f2994a', negative: '#2f80ed' }, inverter_charger: { positive: '#f2994a', negative: '#2f80ed' }, battery: { positive: '#2f80ed', negative: '#f2994a' }, dc: { positive: '#27ae60', negative: '#f2994a' }, }; class EnhancedPowerFlowCardEditor extends HTMLElement { constructor() { super(); this._config = {}; this._hass = undefined; this._initialized = false; this.attachShadow({ mode: 'open' }); } set hass(h) { const first = !this._hass; this._hass = h; // Re-render once when hass first arrives so HA pickers are created with hass. if (first) { this._render(); } // Defer hass application to ensure elements are upgraded first. requestAnimationFrame(() => this._applyHassToPickers()); } setConfig(config) { if (!this._initialized) { this._config = JSON.parse(JSON.stringify(config || {})); this._ensureDefaults(true); this._initialized = true; this._render(); this._applyHassToPickers(); return; } const prev = JSON.stringify(this._config || {}); this._config = JSON.parse(JSON.stringify(config || {})); this._ensureDefaults(false); const current = JSON.stringify(this._config || {}); if (current === prev) { this._applyHassToPickers(); return; } this._render(); this._applyHassToPickers(); } _ensureDefaults(initial = false) { const c = this._config; c.entities = c.entities || {}; for (const k of ['ac_input','ac_output','inverter_charger','battery','dc']) { c.entities[k] = c.entities[k] || {}; } c.flow_colors = c.flow_colors || {}; for (const pathKey of ['inverter_battery','battery_dc','inverter_dc']) { c.flow_colors[pathKey] = c.flow_colors[pathKey] || {}; } if (initial) { if (c.show_defaults === undefined) c.show_defaults = true; if (c.show_background === undefined) c.show_background = true; if (c.line_width === undefined) c.line_width = 2; if (c.ball_diameter === undefined) c.ball_diameter = 4; if (c.corner_radius === undefined) c.corner_radius = 8; if (!c.shape) c.shape = 'oval'; if (c.line_glow_size === undefined) c.line_glow_size = 3; if (c.ball_glow_size === undefined) c.ball_glow_size = 3; if (c.line_glow_brightness === undefined) c.line_glow_brightness = 0.4; if (c.ball_glow_brightness === undefined) c.ball_glow_brightness = 1; if (c.entities.battery.power_unit === undefined) c.entities.battery.power_unit = 'W'; const flowDefaults = { inverter_battery: FLOW_COLOR_DEFAULTS.battery, battery_dc: FLOW_COLOR_DEFAULTS.dc, inverter_dc: FLOW_COLOR_DEFAULTS.dc, }; for (const [pathKey, defaults] of Object.entries(flowDefaults)) { const target = c.flow_colors[pathKey]; if (target.positive === undefined) target.positive = defaults.positive; if (target.negative === undefined) target.negative = defaults.negative; } } } _render() { if (!this.shadowRoot) return; const c = this._config || {}; const e = c.entities || {}; const inverterDisplayName = e.inverter_charger?.name || 'Inverter-Charger'; this.shadowRoot.innerHTML = `
Card Configuration
Appearance
Circle Oval Squircle Square
${this._entitySection('AC Input','ac_input', e.ac_input)} ${this._entitySection('AC Output','ac_output', e.ac_output)} ${this._entitySection('Inverter-Charger','inverter_charger', e.inverter_charger, { showColor: false, showInvert: false, labels: { unit: 'Consumed power unit', secondary: 'Secondary (template ok)', secondary_unit: 'Unit', tertiary: 'Tertiary (template ok)', tertiary_unit: 'Unit' }, includeBatteryFlowControls: { batteryName: e.battery?.name || 'Battery', dcName: e.dc?.name || 'DC System' } })} ${this._batterySection('Battery','battery', e.battery)} ${this._entitySection('DC System','dc', e.dc, { showColor: false, showInverterBatteryColors: false, showInvert: false })}
`; this._wireEvents(); } _entitySection(label, key, cfg = {}, options = {}) { const pos = cfg.positive === true ? 'checked' : ''; const inv = cfg.invert === true ? 'checked' : ''; const opts = { showColor: options.showColor !== false, showInvert: options.showInvert !== false, showInverterBatteryColors: options.showInverterBatteryColors === true, invertLabel: options.invertLabel, includeBatteryFlowControls: options.includeBatteryFlowControls, }; const labels = options.labels || {}; const labelFor = (prop, fallback) => labels[prop] || fallback; const entityLabelAttr = labels.entity ? ` label="${labels.entity}"` : ''; const mainEntityField = ``; const rows = [ [ ``, `` ] ]; let urlField = ''; switch (key) { case 'ac_input': urlField = ``; break; case 'ac_output': urlField = ``; break; case 'inverter_charger': urlField = ``; break; case 'battery': urlField = ``; break; case 'dc': urlField = ``; break; } rows.push([ `
${urlField}
`, '' ]); rows.push([ mainEntityField, `` ]); rows.push([ ``, `` ]); rows.push([ ``, `` ]); if (opts.showInverterBatteryColors) { rows.push([ ``, `` ]); } if (opts.showColor) { rows.push([ ``, `` ]); } if (opts.includeBatteryFlowControls) { const batteryName = opts.includeBatteryFlowControls.batteryName || 'Battery'; const dcName = opts.includeBatteryFlowControls.dcName || 'DC System'; rows.push([ ``, ``, ]); rows.push([ ``, `` ]); rows.push([ ``, '
 
' ]); rows.push([ ``, `` ]); rows.push([ ``, `` ]); } rows.push([ ``, opts.showInvert ? `` : '
 
' ]); const fields = rows.map(([a,b]) => `${a}${b}`).join(''); return `
${label}
${fields}
`; } _batterySection(label, key, cfg = {}) { const pos = cfg.positive === true ? 'checked' : ''; const inv = cfg.invert === true ? 'checked' : ''; const socSelector = ``; const socUnitField = ``; const mainSelector = ``; const mainUnitField = ``; const flowStateSelector = ``; const rows = [ [ ``, `` ], [ socSelector, socUnitField ], [ mainSelector, mainUnitField ], [ ``, `` ], [ ``, `` ], [ ``, ``, ], [ flowStateSelector, '' ], [ ``, `` ], [ ``, ``, ], [ ``, `` ] ]; const fields = rows.map(([a,b]) => `${a}${b}`).join(''); return `
${label}
${fields}
`; } _flowColorInputValue(key, cfg, type, customFieldName = null) { const fieldName = customFieldName || `color_${type}`; const setting = cfg?.[fieldName]; if (setting) { return setting; } const defaults = FLOW_COLOR_DEFAULTS[key] || FLOW_COLOR_DEFAULTS.ac_input; return defaults[type]; } _flowPathColorInputValue(pathKey, type) { const override = this._config?.flow_colors?.[pathKey]; if (override && override[type]) { return override[type]; } let defaults = FLOW_COLOR_DEFAULTS.battery; if (pathKey === 'battery_dc' || pathKey === 'inverter_dc') { defaults = FLOW_COLOR_DEFAULTS.dc; } return defaults[type]; } _formatWithUnit(value, unit) { if (value === undefined || value === null || value === '') return ''; return unit ? `${value} ${unit}` : `${value}`; } _enforcePositiveNumberString(text) { if (text === undefined || text === null) return text; return String(text).replace(/-?\d+\.?\d*/g, (match) => { const num = parseFloat(match); if (Number.isNaN(num)) return match; const decimals = match.includes('.') ? match.split('.')[1].length : 0; return Math.abs(num).toFixed(decimals); }); } _looksLikeEntityId(value) { if (typeof value !== 'string') return false; const trimmed = value.trim(); if (!trimmed.includes('.')) return false; return /^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+$/.test(trimmed); } _getSecondaryDisplayValue(config) { if (!config) return ''; const raw = config.secondary; if (raw === undefined || raw === null) return ''; if (typeof raw === 'string') { const trimmed = raw.trim(); if (!trimmed) return ''; if (trimmed.includes('{{') || trimmed.includes('{%')) { return this._evaluateTemplate(trimmed, config.entity); } if (this._looksLikeEntityId(trimmed)) { const entity = this._hass?.states?.[trimmed]; if (entity && entity.state !== undefined) { return entity.state; } } return trimmed; } return raw; } _getTertiaryDisplayValue(config) { if (!config) return ''; const raw = config.tertiary; if (raw === undefined || raw === null) return ''; if (typeof raw === 'object') { const maybeEntity = typeof raw.entity === 'string' ? raw.entity : (typeof raw.entity_id === 'string' ? raw.entity_id : ''); if (maybeEntity) { const entity = this._hass?.states?.[maybeEntity]; if (entity && entity.state !== undefined && entity.state !== null) { return entity.state; } } return ''; } if (typeof raw === 'string') { const trimmed = raw.trim(); if (!trimmed) return ''; if (trimmed.includes('{{') || trimmed.includes('{%')) { return this._evaluateTemplate(trimmed, config.entity); } if (this._looksLikeEntityId(trimmed)) { const entity = this._hass?.states?.[trimmed]; if (entity && entity.state !== undefined) { return entity.state; } } return trimmed; } return raw; } _convertPythonConditional(expression) { if (!expression) return expression; const pattern = /(.+?)\s+if\s+(.+?)\s+else\s+(.+?)(?=$|\s|,|\)|\])/g; return expression.replace(pattern, (_match, trueExpr, condition, falseExpr) => { return `(${condition} ? ${trueExpr} : ${falseExpr})`; }); } _wireEvents() { const root = this.shadowRoot; if (!root) return; const numberFields = root.querySelectorAll('ha-textfield[type="number"]'); numberFields.forEach((el) => { el.addEventListener('input', (e) => { const path = e.target.getAttribute('data-path'); const val = e.target.value === '' ? undefined : Number(e.target.value); this._set(path, val); }); }); const textFields = root.querySelectorAll('ha-textfield:not([type="number"])'); textFields.forEach((el) => { el.addEventListener('input', (e) => { const path = e.target.getAttribute('data-path'); this._set(path, e.target.value); }); }); const selects = root.querySelectorAll('ha-select'); selects.forEach((el) => { el.addEventListener('selected', (e) => { const path = el.getAttribute('data-path'); this._set(path, el.value); }); el.addEventListener('closed', (e) => e.stopPropagation()); }); const switches = root.querySelectorAll('ha-switch'); switches.forEach((el) => { el.addEventListener('change', (e) => { const path = e.target.getAttribute('data-path'); this._set(path, e.target.checked); }); }); const pickers = root.querySelectorAll('ha-selector'); pickers.forEach((el) => { const path = el.getAttribute('data-path'); const selectorType = el.getAttribute('data-selector') || 'entity'; el.selector = { [selectorType]: {} }; if (this._hass) el.hass = this._hass; const current = this._getValueFromConfig(path); if (current !== undefined) { el.value = current; } el.addEventListener('value-changed', (e) => { this._set(path, e.detail.value); }); }); const iconPickers = root.querySelectorAll('ha-icon-picker'); iconPickers.forEach((el) => { const path = el.getAttribute('data-path'); if (this._hass) el.hass = this._hass; const current = this._getValueFromConfig(path); if (current !== undefined) { el.value = current; } el.addEventListener('value-changed', (e) => { this._set(path, e.detail.value); }); }); } _applyHassToPickers() { if (!this._hass || !this.shadowRoot) return; this.shadowRoot.querySelectorAll('ha-selector').forEach((el) => { el.hass = this._hass; const path = el.getAttribute('data-path'); const val = this._getValueFromConfig(path); if (val !== undefined) el.value = val; }); this.shadowRoot.querySelectorAll('ha-icon-picker').forEach((el) => { el.hass = this._hass; const path = el.getAttribute('data-path'); const val = this._getValueFromConfig(path); if (val !== undefined) el.value = val; }); } _getValueFromConfig(path) { if (!path) return undefined; const parts = path.split('.'); let ref = this._config; for (const p of parts) { if (ref == null) return undefined; ref = ref[p]; } return ref; } _set(path, value) { const newConfig = JSON.parse(JSON.stringify(this._config || {})); const parts = (path || '').split('.'); let ref = newConfig; for (let i = 0; i < parts.length - 1; i++) { const k = parts[i]; if (ref[k] === undefined || typeof ref[k] !== 'object') ref[k] = {}; ref = ref[k]; } const last = parts[parts.length - 1]; if (value === undefined) delete ref[last]; else ref[last] = value; this._config = newConfig; this.dispatchEvent(new CustomEvent('config-changed', { detail: { config: this._config }, bubbles: true, composed: true })); } } customElements.define('enhanced-power-flow-card-editor', EnhancedPowerFlowCardEditor); // Enhanced Power Flow Card for Home Assistant // Version 4: Responsive path recalculation on resize class EnhancedPowerFlowCard extends HTMLElement { static DEBUG = false; static VERSION = CARD_VERSION; constructor() { super(); console.log('%c Enhanced Power Flow Card %c v' + CARD_VERSION + ' ', 'background: #2196F3; color: white; font-weight: bold; padding: 2px 4px;', 'background: #1976D2; color: white; padding: 2px 4px;'); this._debug('Constructor called'); this._renderedOnce = false; this._primedDefaults = false; } static async getConfigElement() { try { if (!customElements.get('enhanced-power-flow-card-editor')) { await import('./enhanced-power-flow-card-editor.js'); } } catch (e) { // no-op: HA will still try to create the element if already registered } return document.createElement('enhanced-power-flow-card-editor'); } static getStubConfig(hass) { return { line_width: 2, ball_diameter: 4, corner_radius: 8, line_glow_size: 3, ball_glow_size: 3, line_glow_brightness: 0.4, ball_glow_brightness: 1, shape: 'oval', entities: { ac_input: { entity: 'sensor.ac_in', name: 'Grid', unit: 'W', icon: 'mdi:transmission-tower' }, ac_output: { entity: 'sensor.ac_out', name: 'AC Output', unit: 'W', icon: 'mdi:home' }, inverter_charger: { entity: 'sensor.inverter', name: 'Inverter-Charger', unit: 'W', icon: 'mdi:sync' }, battery: { entity: 'sensor.battery_soc', power_entity: 'sensor.battery_power', power_unit: 'W', name: 'Battery', unit: '%', icon: 'mdi:battery' }, dc: { entity: 'sensor.dc_power', name: 'DC System', unit: 'W', icon: 'mdi:current-dc' } } }; } _debug(message, data = null) { if (EnhancedPowerFlowCard.DEBUG) { const timestamp = new Date().toISOString(); console.log(`[EnhancedPowerFlowCard ${timestamp}] ${message}`, data || ''); } } setConfig(config) { this._debug('setConfig called', config); if (!config.entities) { throw new Error('You need to define entities'); } if (config.entities && !config.entities.ac_output && config.entities.ac_output) { config.entities.ac_output = config.entities.ac_output; } const prevCfg = this.config || {}; this.config = config; this._applyConfigValues(); const pathChanged = !this._renderedOnce || prevCfg.line_width !== config.line_width || prevCfg.corner_radius !== config.corner_radius || prevCfg.ball_diameter !== config.ball_diameter || prevCfg.shape !== config.shape; if (!this.content) { this.attachShadow({ mode: "open" }); this.content = document.createElement("div"); this.shadowRoot.appendChild(this.content); } if (!this._renderedOnce) { this.render(); this._attachClickHandlers(); this._setupResizeObserver(); this._renderedOnce = true; return; } else { if (pathChanged) { this._createDynamicPaths(); } this.updateValues(); } } _applyConfigValues() { const cfg = this.config || {}; this.lineWidth = cfg.line_width || 2; this.arrowSize = cfg.arrow_size || 3; this.cornerRadius = cfg.corner_radius || 8; this.ballDiameter = cfg.ball_diameter || 4; this.lineGlowSize = cfg.line_glow_size || 3; this.ballGlowSize = cfg.ball_glow_size || 3; this.lineGlowBrightness = cfg.line_glow_brightness || 0.4; this.ballGlowBrightness = cfg.ball_glow_brightness || 1; this.shape = cfg.shape || 'oval'; this.cardTitle = typeof cfg.title === 'string' ? cfg.title.trim() : ''; this.showCardBackground = cfg.show_background !== false; } _setupResizeObserver() { // Recalculate paths on resize with debouncing if (typeof ResizeObserver !== 'undefined') { this._resizeObserver = new ResizeObserver(() => { clearTimeout(this._resizeDebounce); this._resizeDebounce = setTimeout(() => { this._createDynamicPaths(); }, 100); }); setTimeout(() => { const grid = this.shadowRoot.getElementById('grid'); if (grid) { this._resizeObserver.observe(grid); } }, 0); } // Fallback for older browsers this._windowResizeHandler = () => { clearTimeout(this._resizeTimeout); this._resizeTimeout = setTimeout(() => { this._createDynamicPaths(); }, 100); }; window.addEventListener('resize', this._windowResizeHandler); } disconnectedCallback() { // Cleanup resize observers and listeners if (this._resizeObserver) { this._resizeObserver.disconnect(); } if (this._windowResizeHandler) { window.removeEventListener('resize', this._windowResizeHandler); } clearTimeout(this._resizeTimeout); clearTimeout(this._resizeDebounce); } _attachClickHandlers() { setTimeout(() => { const cfg = this.config || {}; const handlers = { 'ac-in-node': cfg.ac_in_url, 'ac-output-node': cfg.ac_output_url, 'inverter-node': cfg.inverter_url, 'battery-node': cfg.battery_url, 'dc-node': cfg.dc_url, }; Object.entries(handlers).forEach(([nodeId, path]) => { const node = this.shadowRoot.getElementById(nodeId); if (!node) return; // if no URL, ensure it is not clickable if (!path || !path.trim()) { node.style.cursor = 'default'; return; } // URL exists, make it clickable node.style.cursor = 'pointer'; node.addEventListener('click', (e) => { e.stopPropagation(); window.history.pushState(null, '', path); window.dispatchEvent(new CustomEvent('location-changed')); }); }); }, 0); } set hass(hass) { this._debug('hass setter called'); this._hass = hass; this.updateValues(); } getCardSize() { return 4; } _evaluateTemplate(template, entityId) { this._debug('_evaluateTemplate', { template, entityId }); if (!template) return null; if (typeof template !== 'string') return template; const vars = {}; let working = template; const setPattern = /\{\%\s*set\s+(\w+)\s*=\s*(.+?)\s*\%\}/g; working = working.replace(setPattern, (full, name, expression) => { const value = this._runTemplateExpression(expression.trim(), entityId, vars); vars[name] = value; return ''; }); if (!working.includes('{{')) { return working.replace(/\s+/g, ' ').trim(); } const result = working.replace(/\{\{\s*(.+?)\s*\}\}/g, (match, expression) => { const value = this._runTemplateExpression(expression.trim(), entityId, vars); return value !== undefined ? value : ''; }); this._debug('Template evaluated', { original: template, result }); return result; } _runTemplateExpression(expression, entityId, vars = {}) { let expr = expression; let precision; const floatMatch = expr.match(/\|\s*float(?:\s*\(\s*(\d+)\s*\))?/); if (floatMatch) { precision = floatMatch[1] !== undefined ? Number(floatMatch[1]) : undefined; } expr = expr.replace(/\|\s*float(?:\([^)]*\))?/g, ''); expr = expr.replace(/states\(['"]([^'"]+)['"]\)/g, (m, eid) => { const value = this._hass?.states?.[eid]?.state; return this._literalForValue(value ?? '0'); }); expr = expr.replace(/states\.([^.]+)\.([^.]+)\.state/g, (m, domain, name) => { const value = this._hass?.states?.[`${domain}.${name}`]?.state; return this._literalForValue(value ?? '0'); }); expr = expr.replace(/states\.([^.]+)\.([^.]+)\.attributes\.([^\s}]+)\s*/g, (m, domain, name, attr) => { const value = this._hass?.states?.[`${domain}.${name}`]?.attributes?.[attr]; return this._literalForValue(value ?? '0'); }); expr = this._convertPythonConditional(expr); const varInit = Object.entries(vars).map(([name, val]) => `const ${name} = ${this._literalForValue(val)};`).join('\n'); try { // eslint-disable-next-line no-eval const res = eval(`${varInit}${expr}`); if (precision !== undefined && typeof res === 'number') { return res.toFixed(precision); } return res; } catch (e) { console.warn('Template math evaluation failed:', e); return undefined; } } _literalForValue(value) { if (value === undefined || value === null) return '0'; if (typeof value === 'number') return `${value}`; if (typeof value === 'string') { return `'${value.replace(/'/g, "\\'")}'`; } return JSON.stringify(value); } _getEntityConfig(entityKey) { this._debug('_getEntityConfig', entityKey); return this.config.entities[entityKey]; } _getEntityValue(config) { const numeric = this._getEntityValueNumber(config); if (numeric === undefined || Number.isNaN(numeric)) return 0; return numeric.toFixed(0); } _getEntityValueNumber(config) { this._debug('_getEntityValueNumber', config); if (!config || !config.entity || !this._hass) return undefined; let value = 0; if (typeof config.entity === 'string') { if (config.entity.includes('{{')) { const evaluated = this._evaluateTemplate(config.entity, null); value = parseFloat(evaluated); } else { const entity = this._hass.states[config.entity]; if (!entity) return undefined; value = parseFloat(entity.state); } } if (Number.isNaN(value)) return undefined; value = this._applyValueTransforms(config, value); return value; } _canCalculateConsumedPower() { const acInput = this._getEntityConfig('ac_input'); const acOutput = this._getEntityConfig('ac_output'); return Boolean(acInput?.entity && acOutput?.entity); } _calculateConsumedPower() { if (!this._hass) return undefined; const acInputConfig = this._getEntityConfig('ac_input'); const acOutputConfig = this._getEntityConfig('ac_output'); if (!acInputConfig || !acOutputConfig) return undefined; const acInput = this._getEntityValueNumber(acInputConfig); const acOutput = this._getEntityValueNumber(acOutputConfig); if (acInput === undefined || acOutput === undefined) return undefined; return acInput - acOutput; } _applyValueTransforms(config, value) { let v = Number(value); if (isNaN(v)) return 0; if (config?.positive === true) v = Math.abs(v); return v; } _getPowerEntityValue(config) { if (!config?.power_entity || !this._hass) return undefined; const st = this._hass.states[config.power_entity]; if (!st) return undefined; const val = parseFloat(st.state); if (isNaN(val)) return undefined; return this._applyValueTransforms(config, val); } _getBatteryChargeState(config) { if (!config?.flow_state_entity || !this._hass) return undefined; const flowStateEntity = config.flow_state_entity.trim(); let raw; // Check if it's a template if (flowStateEntity.includes('{{') || flowStateEntity.includes('{%')) { raw = this._evaluateTemplate(flowStateEntity, config.entity); } else if (this._looksLikeEntityId(flowStateEntity)) { // It's an entity ID const st = this._hass.states[flowStateEntity]; if (!st) return undefined; raw = st.state; } else { // It's a literal value raw = flowStateEntity; } if (raw === undefined || raw === null) return undefined; const val = parseFloat(raw); if (isNaN(val)) return undefined; return val >= 0.5; } _getBatteryFlowStateDisplay(config) { if (!config?.flow_state_entity || !this._hass) return ''; const flowStateEntity = config.flow_state_entity.trim(); let raw; // Check if it's a template if (flowStateEntity.includes('{{') || flowStateEntity.includes('{%')) { raw = this._evaluateTemplate(flowStateEntity, config.entity); } else if (this._looksLikeEntityId(flowStateEntity)) { // It's an entity ID const st = this._hass.states[flowStateEntity]; if (!st) return ''; raw = st.state; } else { // It's a literal value raw = flowStateEntity; } if (raw === undefined || raw === null) return ''; if (raw === 'on') return 'Charging'; if (raw === 'off') return 'Discharging'; if (raw === 'true') return 'Charging'; if (raw === 'false') return 'Discharging'; const numeric = parseFloat(raw); if (!Number.isNaN(numeric)) { return numeric >= 0.5 ? 'Charging' : 'Discharging'; } const trimmed = String(raw).trim(); if (!trimmed) return ''; return trimmed.charAt(0).toUpperCase() + trimmed.slice(1); } render() { this._debug('render called'); const showBackground = this.config?.show_background !== false; const cardClass = `card${showBackground ? '' : ' card-plain'}`; const titleMarkup = this.cardTitle ? `
${this.cardTitle}
` : ''; this.content.innerHTML = `
${titleMarkup}
AC INPUT
-- kW
AC OUTPUT
-- kW
INVERTER/CHARGER
-- kW
BATTERY
-- %
-- W
DC SYSTEM
-- W
`; setTimeout(() => this._createDynamicPaths(), 100); } _getContainerPoint(nodeId, position) { const node = this.shadowRoot.getElementById(nodeId); if (!node) return { x: 0, y: 0 }; const relX = node.offsetLeft; const relY = node.offsetTop; const width = node.offsetWidth; const height = node.offsetHeight; switch(position) { case 'bottom-center': return { x: relX + width / 2, y: relY + height }; case 'left-middle': return { x: relX, y: relY + height / 2 }; case 'right-middle': return { x: relX + width, y: relY + height / 2 }; case 'right-two-thirds-up': return { x: relX + width, y: relY + (height / 3) }; case 'left-two-thirds-up': return { x: relX, y: relY + (height / 3) }; case 'right-one-third-up': return { x: relX + width, y: relY + (height * 2 / 3) }; case 'left-one-third-up': return { x: relX, y: relY + (height * 2 / 3) }; default: return { x: relX, y: relY }; } } _createPath(start, end, cornerRadius) { const dx = end.x - start.x; const dy = end.y - start.y; // Straight horizontal line if (Math.abs(dy) < 0.1) { return `M ${start.x} ${start.y} L ${end.x} ${end.y}`; } // Straight vertical line if (Math.abs(dx) < 0.1) { return `M ${start.x} ${start.y} L ${end.x} ${end.y}`; } const r = Math.min(cornerRadius, Math.abs(dx), Math.abs(dy)); // Going DOWN and RIGHT if (dy > 0 && dx > 0) { return `M ${start.x} ${start.y} L ${start.x} ${end.y - r} Q ${start.x} ${end.y} ${start.x + r} ${end.y} L ${end.x} ${end.y}`; } // Going DOWN and LEFT else if (dy > 0 && dx < 0) { return `M ${start.x} ${start.y} L ${start.x} ${end.y - r} Q ${start.x} ${end.y} ${start.x - r} ${end.y} L ${end.x} ${end.y}`; } // Going UP and RIGHT else if (dy < 0 && dx > 0) { return `M ${start.x} ${start.y} L ${end.x - r} ${start.y} Q ${end.x} ${start.y} ${end.x} ${start.y - r} L ${end.x} ${end.y}`; } // Going UP and LEFT else if (dy < 0 && dx < 0) { return `M ${start.x} ${start.y} L ${end.x + r} ${start.y} Q ${end.x} ${start.y} ${end.x} ${start.y - r} L ${end.x} ${end.y}`; } // Fallback return `M ${start.x} ${start.y} L ${end.x} ${end.y}`; } _createDynamicPaths() { const svg = this.shadowRoot.getElementById('flow-svg'); const grid = this.shadowRoot.getElementById('grid'); if (!svg || !grid) return; const width = grid.offsetWidth; const height = grid.offsetHeight; // If layout isn't ready yet, retry on the next frame so paths get real coordinates if (!width || !height) { requestAnimationFrame(() => this._createDynamicPaths()); return; } svg.setAttribute('viewBox', `0 0 ${width} ${height}`); const p1_start = this._getContainerPoint('ac-in-node', 'bottom-center'); const p1_end = this._getContainerPoint('inverter-node', 'left-middle'); const p2_start = this._getContainerPoint('inverter-node', 'right-middle'); const p2_end = this._getContainerPoint('ac-output-node', 'bottom-center'); const p3_start = this._getContainerPoint('inverter-node', 'bottom-center'); const p3_end = this._getContainerPoint('battery-node', 'right-two-thirds-up'); const p4_start = this._getContainerPoint('inverter-node', 'bottom-center'); const p4_end = this._getContainerPoint('dc-node', 'left-two-thirds-up'); const p5_start = this._getContainerPoint('battery-node', 'right-one-third-up'); const p5_end = this._getContainerPoint('dc-node', 'left-one-third-up'); const path1 = this._createPath(p1_start, p1_end, this.cornerRadius); const path2 = this._createPath(p2_start, p2_end, this.cornerRadius); const path3 = this._createPath(p3_start, p3_end, this.cornerRadius); const path4 = this._createPath(p4_start, p4_end, this.cornerRadius); const path5 = `M ${p5_start.x} ${p5_start.y} L ${p5_end.x} ${p5_end.y}`; svg.innerHTML = ` ${this._createEndpointCircles('flow-ac', p1_start, p1_end)} ${this._createFlowShape('dot-ac', 'flow-dot-ac', 'flow-ac')} ${this._createEndpointCircles('flow-load', p2_start, p2_end)} ${this._createFlowShape('dot-load', 'flow-dot-load', 'flow-load')} ${this._createEndpointCircles('flow-inverter-bat', p3_start, p3_end)} ${this._createFlowShape('dot-inverter-bat', 'flow-dot-inverter-bat', 'flow-inverter-bat')} ${this._createEndpointCircles('flow-dc-in', p4_start, p4_end)} ${this._createFlowShape('dot-dc-in', 'flow-dot-dc-in', 'flow-dc-in')} ${this._createEndpointCircles('flow-dc-out', p5_start, p5_end)} ${this._createFlowShape('dot-dc-out', 'flow-dot-dc-out', 'flow-dc-out')} `; // Apply dynamic stroke widths without full re-render svg.querySelectorAll('.flow').forEach((p) => { p.style.strokeWidth = `${this.lineWidth}`; }); // If defaults are enabled, re-prime once paths exist so they become visible if (!this._primedDefaults && this._primeInitialDefaults && (this.config?.show_defaults !== false)) { try { this._primeInitialDefaults(); this._primedDefaults = true; } catch (e) {} } } _ensurePathsReady() { const svg = this.shadowRoot?.getElementById('flow-svg'); const grid = this.shadowRoot?.getElementById('grid'); if (!svg || !grid) return false; if (!grid.offsetWidth || !grid.offsetHeight) { requestAnimationFrame(() => this._createDynamicPaths()); return false; } if (!svg.childElementCount) { this._createDynamicPaths(); } return !!svg.childElementCount; } _isReversed(config, value) { const invert = config?.invert === true; const signalReverse = value < -0.05; return invert ? !signalReverse : signalReverse; } _getFlowColor(key, config, value) { const defaults = FLOW_COLOR_DEFAULTS[key] || FLOW_COLOR_DEFAULTS.ac_input; const positive = config?.color_positive || defaults.positive; const negative = config?.color_negative || defaults.negative; return value < 0 ? negative : positive; } _getOverrideFlowColor(pathKey, isPositive) { const overrides = this.config?.flow_colors?.[pathKey]; if (!overrides) return null; const color = isPositive ? overrides.positive : overrides.negative; return color || null; } _enforcePositiveNumberString(text) { if (text === undefined || text === null) return text; return String(text).replace(/-?\d+\.?\d*/g, (match) => { const num = parseFloat(match); if (Number.isNaN(num)) return match; const decimals = match.includes('.') ? match.split('.')[1].length : 0; return Math.abs(num).toFixed(decimals); }); } _looksLikeEntityId(value) { if (typeof value !== 'string') return false; const trimmed = value.trim(); if (!trimmed.includes('.')) return false; return /^[a-zA-Z0-9_]+\.[a-zA-Z0-9_]+$/.test(trimmed); } _getSecondaryDisplayValue(config) { if (!config) return ''; const raw = config.secondary; if (raw === undefined || raw === null) return ''; if (typeof raw === 'object') { const maybeEntity = typeof raw.entity === 'string' ? raw.entity : (typeof raw.entity_id === 'string' ? raw.entity_id : ''); if (maybeEntity) { const entity = this._hass?.states?.[maybeEntity]; if (entity && entity.state !== undefined && entity.state !== null) { return entity.state; } } return ''; } if (typeof raw === 'string') { const trimmed = raw.trim(); if (!trimmed) return ''; if (trimmed.includes('{{') || trimmed.includes('{%')) { return this._evaluateTemplate(trimmed, config.entity); } if (this._looksLikeEntityId(trimmed)) { const entity = this._hass?.states?.[trimmed]; if (entity && entity.state !== undefined) { return entity.state; } } return trimmed; } return raw; } _getTertiaryDisplayValue(config) { if (!config) return ''; const raw = config.tertiary; if (raw === undefined || raw === null) return ''; if (typeof raw === 'object') { const maybeEntity = typeof raw.entity === 'string' ? raw.entity : (typeof raw.entity_id === 'string' ? raw.entity_id : ''); if (maybeEntity) { const entity = this._hass?.states?.[maybeEntity]; if (entity && entity.state !== undefined && entity.state !== null) { return entity.state; } } return ''; } if (typeof raw === 'string') { const trimmed = raw.trim(); if (!trimmed) return ''; if (trimmed.includes('{{') || trimmed.includes('{%')) { return this._evaluateTemplate(trimmed, config.entity); } if (this._looksLikeEntityId(trimmed)) { const entity = this._hass?.states?.[trimmed]; if (entity && entity.state !== undefined) { return entity.state; } } return trimmed; } return raw; } _formatWithUnit(value, unit) { if (value === undefined || value === null || value === '') return ''; return unit ? `${value} ${unit}` : `${value}`; } _convertPythonConditional(expression) { if (!expression) return expression; const pattern = /(.+?)\s+if\s+(.+?)\s+else\s+(.+?)(?=$|\s|,|\)|\])/g; return expression.replace(pattern, (_match, trueExpr, condition, falseExpr) => { return `(${condition} ? ${trueExpr} : ${falseExpr})`; }); } _evaluateVisibilityTemplate(template, entityId) { if (template === undefined || template === null) return undefined; const trimmed = String(template).trim(); if (!trimmed) return undefined; let value = trimmed; if (trimmed.includes('{{') || trimmed.includes('{%')) { value = this._evaluateTemplate(trimmed, entityId); } else if (this._looksLikeEntityId(trimmed)) { value = this._hass?.states?.[trimmed]?.state; } return this._coerceBoolean(value); } _coerceBoolean(value) { if (value === undefined || value === null) return undefined; if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; const str = String(value).trim().toLowerCase(); if (!str) return undefined; if (['on','true','1','open','enabled','yes','active','charging'].includes(str)) return true; if (['off','false','0','closed','disabled','no','inactive','discharging'].includes(str)) return false; return undefined; } _createFlowShape(id, className, pathId) { const r = this.ballDiameter / 2; switch(this.shape) { case 'circle': return ` `; case 'oval': return ` `; case 'squircle': const size = r * 1.8; return ` `; case 'square': const squareSize = r * 1.6; return ` `; default: return ` `; } } _createEndpointCircles(flowId, startPoint, endPoint) { const r = this.lineWidth * 1.5; return ` `; } _renderIcon(iconName) { this._debug('_renderIcon', iconName); if (!iconName) return ''; return ``; } updateValues() { this._debug('updateValues called'); if (!this._hass) { this._debug('No hass object, skipping update'); return; } this._updateNode('ac_input', 'ac-in'); this._updateNode('ac_output', 'ac-output'); this._updateNode('inverter_charger', 'inverter'); this._updateNode('battery', 'battery'); this._updateNode('dc', 'dc'); this._updateFlows(); } _updateNode(configKey, nodePrefix) { this._debug('_updateNode', { configKey, nodePrefix }); const config = this._getEntityConfig(configKey); const nodeEl = this.shadowRoot.getElementById(`${nodePrefix}-node`); if (!config) { nodeEl?.classList?.add('hidden'); return; } if (configKey === 'battery') { const hasBatteryEntity = Boolean(config.entity || config.power_entity); if (!hasBatteryEntity) { nodeEl?.classList?.add('hidden'); return; } nodeEl?.classList?.remove('hidden'); this._updateBatteryNode(config, nodePrefix); return; } if (configKey === 'inverter_charger') { const hasPrimarySource = Boolean(config.entity) || this._canCalculateConsumedPower(); if (!hasPrimarySource && !config.secondary && !config.tertiary) { nodeEl?.classList?.add('hidden'); return; } nodeEl?.classList?.remove('hidden'); this._updateInverterNode(config, nodePrefix); return; } if (!config.entity) { nodeEl?.classList?.add('hidden'); return; } nodeEl?.classList?.remove('hidden'); const titleEl = this.shadowRoot.getElementById(`${nodePrefix}-title`); if (titleEl) { const name = this._evaluateTemplate(config.name, config.entity); if (name) { titleEl.textContent = name; titleEl.style.display = ''; } else { titleEl.textContent = ''; titleEl.style.display = 'none'; } } const iconEl = this.shadowRoot.getElementById(`${nodePrefix}-icon`); if (iconEl) { const icon = this._evaluateTemplate(config.icon, config.entity); if (icon) { iconEl.innerHTML = this._renderIcon(icon); iconEl.style.display = ''; } else { iconEl.innerHTML = ''; iconEl.style.display = 'none'; } } const value = this._getEntityValue(config); const valueEl = this.shadowRoot.getElementById(`${nodePrefix}-value`); if (valueEl) { const displayText = `${value} ${config.unit}`; valueEl.textContent = displayText; const length = displayText.length; if (length > 10) { valueEl.style.fontSize = `${Math.max(0.9, 1.8 - (length - 10) * 0.08)}em`; } else { valueEl.style.fontSize = ''; } if (configKey === 'ac_input') { const nodeEl = this.shadowRoot.getElementById(`${nodePrefix}-node`); if (nodeEl) { const numValue = parseFloat(value); if (numValue <= 0.05) { nodeEl.classList.add('desaturated'); } else { nodeEl.classList.remove('desaturated'); } } } } const secondaryEl = this.shadowRoot.getElementById(`${nodePrefix}-secondary`); if (secondaryEl) { const secondaryValue = this._getSecondaryDisplayValue(config); let preparedSecondary = secondaryValue !== undefined && secondaryValue !== null ? `${secondaryValue}` : ''; if (config.positive === true && preparedSecondary) { preparedSecondary = this._enforcePositiveNumberString(preparedSecondary); } const secondaryText = this._formatWithUnit(preparedSecondary, config.secondary_unit); secondaryEl.textContent = secondaryText; secondaryEl.style.display = secondaryText ? 'block' : 'none'; } const tertiaryEl = this.shadowRoot.getElementById(`${nodePrefix}-tertiary`); if (tertiaryEl && config.tertiary) { let tertiary = this._evaluateTemplate(config.tertiary, config.entity); let preparedTertiary = tertiary !== undefined && tertiary !== null ? `${tertiary}` : ''; if (config.positive === true && preparedTertiary) { preparedTertiary = this._enforcePositiveNumberString(preparedTertiary); } const tertiaryText = this._formatWithUnit(preparedTertiary, config.tertiary_unit); tertiaryEl.textContent = tertiaryText; tertiaryEl.style.display = tertiaryText ? 'block' : 'none'; } } _updateInverterNode(config, nodePrefix) { const titleEl = this.shadowRoot.getElementById(`${nodePrefix}-title`); if (titleEl) { const name = this._evaluateTemplate(config.name, config.entity); if (name) { titleEl.textContent = name; titleEl.style.display = ''; } else { titleEl.textContent = ''; titleEl.style.display = 'none'; } } const iconEl = this.shadowRoot.getElementById(`${nodePrefix}-icon`); if (iconEl) { const icon = this._evaluateTemplate(config.icon, config.entity); if (icon) { iconEl.innerHTML = this._renderIcon(icon); iconEl.style.display = ''; } else { iconEl.innerHTML = ''; iconEl.style.display = 'none'; } } const valueEl = this.shadowRoot.getElementById(`${nodePrefix}-value`); if (valueEl) { let displayText = '--'; if (config.entity) { const value = this._getEntityValue(config); displayText = this._formatWithUnit(value, config.unit); } else { const computed = this._calculateConsumedPower(); if (computed !== undefined) { const unit = config.unit || 'W'; displayText = this._formatWithUnit(computed.toFixed(0), unit); } } valueEl.textContent = displayText || '--'; const length = (displayText || '').length; if (length > 10) { valueEl.style.fontSize = `${Math.max(0.9, 1.8 - (length - 10) * 0.08)}em`; } else { valueEl.style.fontSize = ''; } } const secondaryEl = this.shadowRoot.getElementById(`${nodePrefix}-secondary`); if (secondaryEl) { const secondaryValue = this._getSecondaryDisplayValue(config); let prepared = secondaryValue !== undefined && secondaryValue !== null ? `${secondaryValue}` : ''; if (config.positive === true && prepared) { prepared = this._enforcePositiveNumberString(prepared); } const secondaryText = this._formatWithUnit(prepared, config.secondary_unit); secondaryEl.textContent = secondaryText; secondaryEl.style.display = secondaryText ? 'block' : 'none'; } const tertiaryEl = this.shadowRoot.getElementById(`${nodePrefix}-tertiary`); if (tertiaryEl) { let tertiaryText = ''; if (config.tertiary) { let tertiary = this._evaluateTemplate(config.tertiary, config.entity); let preparedTertiary = tertiary !== undefined && tertiary !== null ? `${tertiary}` : ''; if (config.positive === true && preparedTertiary) { preparedTertiary = this._enforcePositiveNumberString(preparedTertiary); } tertiaryText = this._formatWithUnit(preparedTertiary, config.tertiary_unit); } tertiaryEl.textContent = tertiaryText; tertiaryEl.style.display = tertiaryText ? 'block' : 'none'; } } _updateBatteryNode(config, nodePrefix) { this._debug('_updateBatteryNode', { config, nodePrefix }); const powerNumeric = this._getPowerEntityValue(config); const hasPowerEntity = Boolean(config.power_entity); const chargeStateEl = this.shadowRoot.getElementById(`${nodePrefix}-charge-state`); if (chargeStateEl) { const statusText = this._getBatteryFlowStateDisplay(config); chargeStateEl.textContent = statusText; chargeStateEl.style.display = statusText ? 'block' : 'none'; } const titleEl = this.shadowRoot.getElementById(`${nodePrefix}-title`); if (titleEl) { const name = this._evaluateTemplate(config.name, config.entity); if (name) { titleEl.textContent = name; titleEl.style.display = ''; } else { titleEl.textContent = ''; titleEl.style.display = 'none'; } } const iconEl = this.shadowRoot.getElementById(`${nodePrefix}-icon`); if (iconEl) { const icon = this._evaluateTemplate(config.icon, config.entity); if (icon) { iconEl.innerHTML = this._renderIcon(icon); iconEl.style.display = ''; } else { iconEl.innerHTML = ''; iconEl.style.display = 'none'; } } const valueEl = this.shadowRoot.getElementById(`${nodePrefix}-value`); if (valueEl) { const hasSoc = Boolean(config.entity); const value = hasSoc ? this._getEntityValue(config) : '--'; const displayText = this._formatWithUnit(value, config.unit); const textContent = displayText || '--'; valueEl.textContent = textContent; const length = textContent.length; if (length > 10) { valueEl.style.fontSize = `${Math.max(0.9, 1.8 - (length - 10) * 0.08)}em`; } else { valueEl.style.fontSize = ''; } } const powerEl = this.shadowRoot.getElementById(`${nodePrefix}-power`); if (powerEl) { if (!hasPowerEntity) { powerEl.style.display = 'none'; } else { powerEl.style.display = ''; let displayValue = '--'; if (powerNumeric !== undefined && !isNaN(powerNumeric)) { displayValue = powerNumeric.toFixed(0); } const unit = config.power_unit || 'W'; const displayText = this._formatWithUnit(displayValue, unit); powerEl.textContent = displayText; const length = displayText.length; if (length > 8) { powerEl.style.fontSize = `${Math.max(0.7, 1.2 - (length - 8) * 0.06)}em`; } else { powerEl.style.fontSize = ''; } } } const secondaryEl = this.shadowRoot.getElementById(`${nodePrefix}-secondary`); if (secondaryEl) { const secondaryValue = this._getSecondaryDisplayValue(config); let prepared = secondaryValue !== undefined && secondaryValue !== null ? `${secondaryValue}` : ''; if (config.positive === true && prepared) { prepared = this._enforcePositiveNumberString(prepared); } const secondaryText = this._formatWithUnit(prepared, config.secondary_unit); secondaryEl.textContent = secondaryText; secondaryEl.style.display = secondaryText ? 'block' : 'none'; } const tertiaryEl = this.shadowRoot.getElementById(`${nodePrefix}-tertiary`); if (tertiaryEl) { let tertiaryText = ''; if (config.tertiary) { const tertiary = this._evaluateTemplate(config.tertiary, config.entity); let preparedTertiary = tertiary !== undefined && tertiary !== null ? `${tertiary}` : ''; if (config.positive === true && preparedTertiary) { preparedTertiary = this._enforcePositiveNumberString(preparedTertiary); } tertiaryText = this._formatWithUnit(preparedTertiary, config.tertiary_unit); } tertiaryEl.textContent = tertiaryText; tertiaryEl.style.display = tertiaryText ? 'block' : 'none'; } } _updateFlows() { this._debug('_updateFlows called'); if (!this._ensurePathsReady()) { setTimeout(() => this._updateFlows(), 50); return; } const acInputConfig = this._getEntityConfig('ac_input'); const acOutputConfig = this._getEntityConfig('ac_output'); const batteryConfig = this._getEntityConfig('battery'); const dcConfig = this._getEntityConfig('dc'); const inverterConfig = this._getEntityConfig('inverter_charger'); const acInputValue = parseFloat(this._getEntityValue(acInputConfig)) || 0; const acOutputValue = parseFloat(this._getEntityValue(acOutputConfig)) || 0; const inverterValue = parseFloat(this._getEntityValue(inverterConfig)) || 0; const batteryVisible = Boolean(batteryConfig?.entity || batteryConfig?.power_entity); let batteryValue = 0; if (batteryVisible) { if (batteryConfig.power_entity) { batteryValue = this._getPowerEntityValue(batteryConfig) ?? 0; } else { batteryValue = parseFloat(this._getEntityValue(batteryConfig)) || 0; } } const dcValue = parseFloat(this._getEntityValue(dcConfig)) || 0; const baseReverseDcFlow = dcValue < -0.05; let reverseDcFlow = baseReverseDcFlow; if (dcConfig?.invert === true) { reverseDcFlow = !reverseDcFlow; } let reverseBatteryDcFlow = baseReverseDcFlow; if (batteryConfig?.invert_to_battery === true) { reverseBatteryDcFlow = !reverseBatteryDcFlow; } const reverseAcInput = this._isReversed(acInputConfig, acInputValue); const reverseLoad = this._isReversed(acOutputConfig, acOutputValue); const acColor = this._getFlowColor('ac_input', acInputConfig, acInputValue); const loadColor = this._getFlowColor('ac_output', acOutputConfig, acOutputValue); if (!acInputConfig?.entity) { this._hideFlow('flow-ac'); } else { this._showFlow('flow-ac'); this._setFlowState('flow-ac', acInputValue, reverseAcInput, acColor, 5000); } if (!acOutputConfig?.entity) { this._hideFlow('flow-load'); } else { this._showFlow('flow-load'); this._setFlowState('flow-load', acOutputValue, reverseLoad, loadColor, 5000); } const inverterTertiaryRaw = this._getTertiaryDisplayValue(inverterConfig); const inverterMode = typeof inverterTertiaryRaw === 'string' ? inverterTertiaryRaw.trim().toLowerCase() : ''; const inverterToBatteryStates = ['bulk','absorption','float']; const batteryToInverterStates = ['inverting','assisting','power supply']; let inverterFlowDirectionBase = 0; if (inverterToBatteryStates.includes(inverterMode)) { inverterFlowDirectionBase = 1; } else if (batteryToInverterStates.includes(inverterMode)) { inverterFlowDirectionBase = -1; } let inverterFlowDirection = inverterFlowDirectionBase; if (batteryConfig?.invert === true && inverterFlowDirectionBase !== 0) { inverterFlowDirection *= -1; } let batteryColor = null; if (batteryVisible && inverterFlowDirectionBase !== 0) { const directionPositive = inverterFlowDirectionBase > 0; const overrideColor = this._getOverrideFlowColor('inverter_battery', directionPositive); if (overrideColor) { batteryColor = overrideColor; } else if (inverterConfig?.inverter_battery_color_positive && inverterConfig?.inverter_battery_color_negative) { batteryColor = directionPositive ? inverterConfig.inverter_battery_color_positive : inverterConfig.inverter_battery_color_negative; } else { batteryColor = this._getFlowColor('battery', batteryConfig, directionPositive ? 1 : -1); } } if (!batteryVisible) { this._hideFlow('flow-inverter-bat'); } else if (inverterFlowDirection === 0) { this._showFlow('flow-inverter-bat'); const pathEl = this.shadowRoot?.getElementById('flow-inverter-bat'); const dotEl = this.shadowRoot?.getElementById('dot-inverter-bat'); if (pathEl) { pathEl.classList.remove('active'); pathEl.classList.add('inactive'); pathEl.style.display = ''; pathEl.style.stroke = '#666666'; pathEl.style.filter = 'none'; } if (dotEl) { dotEl.style.display = 'none'; dotEl.classList.remove('active'); } } else { this._showFlow('flow-inverter-bat'); const flowVal = Math.abs(batteryValue) || 0.5; const reverseFlow = inverterFlowDirection < 0; this._setFlowState('flow-inverter-bat', flowVal, reverseFlow, batteryColor, 1800); } const dcColor = this._getFlowColor('dc', dcConfig, dcValue); const inverterDcOverrideColor = this._getOverrideFlowColor('inverter_dc', dcValue >= 0); const inverterDcColor = inverterDcOverrideColor || dcColor; const batteryDcOverrideColor = this._getOverrideFlowColor('battery_dc', dcValue >= 0); const batteryDcColor = batteryDcOverrideColor || dcColor; const inverterNodeEl = this.shadowRoot?.getElementById('inverter-node'); const batteryNodeEl = this.shadowRoot?.getElementById('battery-node'); const dcNodeEl = this.shadowRoot?.getElementById('dc-node'); const inverterNodeVisible = Boolean(inverterNodeEl && !inverterNodeEl.classList.contains('hidden')); const batteryNodeVisible = Boolean(batteryNodeEl && !batteryNodeEl.classList.contains('hidden')); const dcNodeVisible = Boolean(dcNodeEl && !dcNodeEl.classList.contains('hidden')); const inverterDcTemplate = this._evaluateVisibilityTemplate(inverterConfig?.dc_flow_template, inverterConfig?.entity); const batteryDcTemplate = this._evaluateVisibilityTemplate(batteryConfig?.dc_flow_template, batteryConfig?.entity); const inverterFlowActive = Boolean(inverterDcTemplate); const batteryFlowActive = batteryDcTemplate === undefined ? false : Boolean(batteryDcTemplate); const manualFlowMagnitude = Math.max( Math.abs(dcValue), Math.abs(batteryValue), Math.abs(inverterValue), 1 ); const inverterFlowValue = inverterFlowActive ? manualFlowMagnitude : 0; const batteryFlowValue = batteryFlowActive ? manualFlowMagnitude : 0; if (!dcNodeVisible) { this._hideFlow('flow-dc-in'); this._hideFlow('flow-dc-out'); } else { if (!inverterNodeVisible) { this._hideFlow('flow-dc-in'); } else { this._showFlow('flow-dc-in'); if (!inverterFlowActive) { this._setFlowDormant('flow-dc-in'); } else { this._setFlowState('flow-dc-in', inverterFlowValue, reverseDcFlow, inverterDcColor, 1000); } } if (!batteryNodeVisible || !batteryVisible) { this._hideFlow('flow-dc-out'); } else { this._showFlow('flow-dc-out'); if (!batteryFlowActive) { this._setFlowDormant('flow-dc-out'); } else { this._setFlowState('flow-dc-out', batteryFlowValue, reverseBatteryDcFlow, batteryDcColor, 360); } } } } _setFlowState(flowId, value, reverse, color = null, maxRange = 3500) { this._debug('_setFlowState', { flowId, value, reverse, color, maxRange }); const pathEl = this.shadowRoot.getElementById(flowId); const dotId = flowId.replace('flow-', 'dot-'); const dotEl = this.shadowRoot.getElementById(dotId); const endpointCircles = this.shadowRoot.querySelectorAll(`.endpoint-circle[data-flow="${flowId}"]`); if (pathEl) pathEl.style.display = ''; if (dotEl) dotEl.style.display = ''; endpointCircles.forEach(circle => circle.style.display = ''); if (!pathEl || !dotEl) return; const absValue = Math.abs(value); const threshold = 0.05; if (absValue > threshold) { pathEl.classList.add('active'); pathEl.classList.remove('inactive'); endpointCircles.forEach(circle => { circle.classList.add('active'); circle.classList.remove('inactive'); }); if (color) { pathEl.style.stroke = color; dotEl.style.color = color; endpointCircles.forEach(circle => circle.style.color = color); } const lineColor = color || window.getComputedStyle(pathEl).stroke; const lineColorMatch = lineColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)|#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/); let r, g, b; if (lineColorMatch) { if (lineColorMatch[1]) { r = lineColorMatch[1]; g = lineColorMatch[2]; b = lineColorMatch[3]; } else { r = parseInt(lineColorMatch[4], 16); g = parseInt(lineColorMatch[5], 16); b = parseInt(lineColorMatch[6], 16); } const glowIntensity = this.lineGlowBrightness; pathEl.style.filter = ` drop-shadow(0 0 ${this.lineGlowSize * 0.5}px rgba(${r}, ${g}, ${b}, ${glowIntensity})) drop-shadow(0 0 ${this.lineGlowSize}px rgba(${r}, ${g}, ${b}, ${glowIntensity * 0.8})) drop-shadow(0 0 ${this.lineGlowSize * 1.5}px rgba(${r}, ${g}, ${b}, ${glowIntensity * 0.4})) `.trim(); } const ballColor = color || window.getComputedStyle(dotEl).color; const ballColorMatch = ballColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)|#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})/); if (ballColorMatch) { let br, bg, bb; if (ballColorMatch[1]) { br = ballColorMatch[1]; bg = ballColorMatch[2]; bb = ballColorMatch[3]; } else { br = parseInt(ballColorMatch[4], 16); bg = parseInt(ballColorMatch[5], 16); bb = parseInt(ballColorMatch[6], 16); } const ballGlowIntensity = this.ballGlowBrightness; dotEl.style.filter = ` drop-shadow(0 0 ${this.ballGlowSize * 0.5}px rgba(${br}, ${bg}, ${bb}, ${ballGlowIntensity})) drop-shadow(0 0 ${this.ballGlowSize}px rgba(${br}, ${bg}, ${bb}, ${ballGlowIntensity * 0.8})) drop-shadow(0 0 ${this.ballGlowSize * 1.5}px rgba(${br}, ${bg}, ${bb}, ${ballGlowIntensity * 0.6})) `.trim(); } dotEl.style.opacity = 1; const maxPower = Math.max(0.001, maxRange || 3500); const minDuration = 1.5; const maxDuration = 5; const pct = Math.min(1, Math.max(0, absValue / maxPower)); const duration = Math.max(minDuration, maxDuration - (pct * (maxDuration - minDuration))); const animateMotion = dotEl.querySelector('animateMotion'); if (animateMotion) { const currentDur = parseFloat(animateMotion.getAttribute('dur')) || 0; if (Math.abs(duration - currentDur) > 0.3) { animateMotion.setAttribute('dur', `${duration}s`); } animateMotion.setAttribute('keyPoints', reverse ? '1;0' : '0;1'); animateMotion.setAttribute('keyTimes', '0;1'); } dotEl.classList.add('active'); } else { pathEl.classList.remove('active'); pathEl.classList.add('inactive'); pathEl.style.filter = 'none'; endpointCircles.forEach(circle => { circle.classList.remove('active'); circle.classList.add('inactive'); }); dotEl.style.opacity = 0; setTimeout(() => { if (dotEl.style.opacity === '0') { dotEl.classList.remove('active'); dotEl.style.filter = 'none'; } }, 500); } } _setFlowDormant(flowId) { const pathEl = this.shadowRoot?.getElementById(flowId); const dotEl = this.shadowRoot?.getElementById(flowId.replace('flow-', 'dot-')); const endpointCircles = this.shadowRoot?.querySelectorAll(`.endpoint-circle[data-flow="${flowId}"]`); if (!pathEl) return; pathEl.style.display = ''; pathEl.classList.remove('active'); pathEl.classList.add('inactive'); pathEl.style.stroke = '#666666'; pathEl.style.filter = 'none'; endpointCircles?.forEach(circle => { circle.style.display = ''; circle.classList.remove('active'); circle.classList.add('inactive'); circle.style.fill = '#666666'; }); if (dotEl) { dotEl.style.display = 'none'; dotEl.classList.remove('active'); dotEl.style.filter = 'none'; } } _hideFlow(flowId) { const pathEl = this.shadowRoot?.getElementById(flowId); const dotEl = this.shadowRoot?.getElementById(flowId.replace('flow-', 'dot-')); const endpointCircles = this.shadowRoot?.querySelectorAll(`.endpoint-circle[data-flow="${flowId}"]`); if (pathEl) { pathEl.style.display = 'none'; pathEl.classList.remove('active'); pathEl.classList.add('inactive'); } if (dotEl) { dotEl.style.display = 'none'; dotEl.style.opacity = 0; dotEl.classList.remove('active'); } endpointCircles?.forEach(circle => { circle.style.display = 'none'; circle.classList.remove('active'); }); } _showFlow(flowId) { const pathEl = this.shadowRoot?.getElementById(flowId); const dotEl = this.shadowRoot?.getElementById(flowId.replace('flow-', 'dot-')); const endpointCircles = this.shadowRoot?.querySelectorAll(`.endpoint-circle[data-flow="${flowId}"]`); if (pathEl) pathEl.style.display = ''; if (dotEl) dotEl.style.display = ''; endpointCircles?.forEach(circle => circle.style.display = ''); } } try { window.customCards = window.customCards || []; window.customCards.push({ type: 'enhanced-power-flow-card', name: 'Enhanced Power Flow Card', description: 'Responsive power flow visual with animated paths', }); } catch (e) { /* ignore in non-HA contexts */ } customElements.define('enhanced-power-flow-card', EnhancedPowerFlowCard); // --- Bootstrap: ensure editor is used without dynamic import and prime defaults (configurable) --- (function(){ const EPFC = customElements.get('enhanced-power-flow-card'); if (!EPFC) return; // Replace getConfigElement to avoid dynamic import EPFC.getConfigElement = async function() { if (!customElements.get('enhanced-power-flow-card-editor')) { // Editor class is defined above in this bundle } return document.createElement('enhanced-power-flow-card-editor'); }; // Ensure stub config includes show_defaults by default const _origStub = EPFC.getStubConfig; EPFC.getStubConfig = function(hass){ const base = _origStub ? _origStub(hass) : { line_width: 2, ball_diameter: 4, corner_radius: 8, line_glow_size: 3, ball_glow_size: 3, line_glow_brightness: 0.4, ball_glow_brightness: 1, shape: 'oval', entities: { ac_input: { entity: 'sensor.ac_in', name: 'Grid', unit: 'W', icon: 'mdi:transmission-tower' }, ac_output: { entity: 'sensor.ac_out', name: 'AC Output', unit: 'W', icon: 'mdi:home' }, inverter_charger: { entity: 'sensor.inverter', name: 'Inverter-Charger', unit: 'W', icon: 'mdi:sync' }, battery: { entity: 'sensor.battery_soc', power_entity: 'sensor.battery_power', power_unit: 'W', name: 'Battery', unit: '%', icon: 'mdi:battery' }, dc: { entity: 'sensor.dc_power', name: 'DC System', unit: 'W', icon: 'mdi:current-dc' } } }; if (base.show_defaults === undefined) base.show_defaults = true; return base; }; // Prime defaults so lines show before hass arrives (guarded by config.show_defaults !== false) EPFC.prototype._primeInitialDefaults = function() { try { if (this.config && this.config.show_defaults === false) return; const ents = (this.config && this.config.entities) || {}; const unitOr = (cfg, d) => (cfg && cfg.unit) ? cfg.unit : d; const acInUnit = unitOr(ents.ac_input, 'W'); const acLoadUnit = unitOr(ents.ac_output, 'W'); const invUnit = unitOr(ents.inverter_charger, 'W'); const dcUnit = unitOr(ents.dc, 'W'); const batUnit = unitOr(ents.battery, '%'); const setText = (id, text) => { const el = this.shadowRoot && this.shadowRoot.getElementById(id); if (el) el.textContent = text; }; const hide = (id) => { const el = this.shadowRoot && this.shadowRoot.getElementById(id); if (el) el.style.display = 'none'; }; setText('ac-in-value', 1 ); setText('ac-output-value', 1 ); setText('inverter-value', 1 ); setText('dc-value', 1 ); setText('battery-value', 1 ); setText('battery-power', '1 W'); this._setFlowState && this._setFlowState('flow-ac', 1, false); this._setFlowState && this._setFlowState('flow-load', 1, false); this._setFlowState && this._setFlowState('flow-inverter-bat', 1, false); this._setFlowState && this._setFlowState('flow-dc-in', 1, false); this._setFlowState && this._setFlowState('flow-dc-out', 1, false); } catch (e) { /* ignore */ } }; // Wrap render to call prime after render const _origRender = EPFC.prototype.render; if (_origRender) { EPFC.prototype.render = function() { _origRender.call(this); setTimeout(() => { try { this._createDynamicPaths && this._createDynamicPaths(); } catch (e) {} try { this._primeInitialDefaults && this._primeInitialDefaults(); } catch (e) {} }, 100); }; } })();