const LUMA_VACUUM_VERSION = '0.2.0'; class LumaVacuumCard extends HTMLElement { set hass(hass) { this._hass = hass; this._render(); } setConfig(config) { if (!config.entity) throw new Error('Entity is required'); this._config = { entity: config.entity, name: config.name || '', size: config.size || '1x2', accent_color: config.accent_color || '', icon: config.icon || '', image: config.image || '', tap_action: config.tap_action || { action: 'more-info' }, hold_action: config.hold_action || { action: 'more-info' }, ...config }; this._render(); } getCardSize() { return 4; } static getStubConfig() { return { entity: '', name: '', size: '1x2' }; } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); } _getState() { return this._hass?.states?.[this._config?.entity]; } _getPrefix() { return this._config?.entity?.replace(/^vacuum\./, '') || ''; } _getSensorState(suffix) { const id = `sensor.${this._getPrefix()}_${suffix}`; return this._hass?.states?.[id]; } _callService(service, data = {}) { if (!this._hass || !this._config) return; this._hass.callService('vacuum', service, { entity_id: this._config.entity, ...data }); } _handleClick() { if (!this._hass || !this._config) return; const ev = new Event('hass-more-info', { composed: true }); ev.detail = { entityId: this._config.entity }; this.dispatchEvent(ev); } _render() { if (!this.shadowRoot || !this._config) return; const s = this._getState(); const prefix = this._getPrefix(); const statusSensor = this._getSensorState('status'); const statusText = statusSensor?.state || s?.state || 'unavailable'; const isCleaning = ['cleaning', 'segment_cleaning', 'zoned_cleaning', 'spot_cleaning', 'segment_mopping', 'robot_status_mopping'].includes(statusText); const isReturning = ['returning_home', 'docking'].includes(statusText); const errorSensor = this._getSensorState('vacuum_error'); const hasError = errorSensor && errorSensor.state !== 'none' && errorSensor.state !== ''; const batterySensor = this._getSensorState('battery'); const battery = batterySensor ? parseInt(batterySensor.state) || 0 : 0; const accent = hasError ? '#e05050' : isCleaning ? '#7EB8DA' : isReturning ? '#f0a060' : 'rgba(157,170,184,0.7)'; const displayStatus = { charging: 'Charged', charging_complete: 'Charged', cleaning: 'Cleaning', segment_cleaning: 'Cleaning', zoned_cleaning: 'Cleaning', returning_home: 'Returning', docking: 'Returning', washing_the_mop: 'Washing mop', going_to_wash_the_mop: 'Washing mop', emptying_the_bin: 'Emptying bin', docked: 'Docked', idle: 'Idle', paused: 'Paused', error: 'Error', }[statusText] || statusText; const statusLine = hasError ? `⚠ ${errorSensor.state.replace(/_/g, ' ')} · ${battery}%` : `${displayStatus} · ${battery}%`; const statusColor = hasError ? '#e05050' : isCleaning ? accent : 'rgba(232,232,232,0.50)'; const name = this._config.name || s?.attributes?.friendly_name || ''; const glowHtml = isCleaning ? '
' : hasError ? '
' : ''; const actions = ['Dock', 'Mop Only', 'V&M Fast', 'After Meals', 'Vac & Mop', 'Custom']; const actionsHtml = actions.map((label, i) => { const cls = i === 5 ? 'action-btn custom' : 'action-btn'; return ``; }).join(''); this.shadowRoot.innerHTML = `
${glowHtml}
${name}
${statusLine}
${this._config.image ? `` : `
`}
${actionsHtml}
`; this.shadowRoot.querySelector('.header').addEventListener('click', () => this._handleClick()); this.shadowRoot.querySelectorAll('.action-btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const idx = parseInt(btn.dataset.idx); switch (idx) { case 0: this._callService('return_to_base'); break; case 5: this._handleClick(); break; default: this._handleClick(); break; } }); }); } } customElements.define('luma-vacuum-card', LumaVacuumCard); window.customCards = window.customCards || []; window.customCards.push({ type: 'luma-vacuum-card', name: 'Luma Vacuum', description: 'Luma-style vacuum card with product image, battery, status, and 6-button action grid.', preview: true }); console.info(`%c LUMA-VACUUM-CARD %c v${LUMA_VACUUM_VERSION} `, 'background:#7EB8DA;color:#111;font-weight:bold', 'background:#161a22;color:#7EB8DA');