// Light Button Card for Home Assistant // Based on: https://developers.home-assistant.io/docs/frontend/custom-ui/custom-card/ class LightButtonCard extends HTMLElement { set hass(hass) { this._hass = hass; this._updateContent(); } setConfig(config) { if (!config.button_entity || !config.binary_sensor_entity) { throw new Error('button_entity and binary_sensor_entity are required'); } this._config = { icon: 'mdi:lightbulb', name: '', show_name: true, show_status: true, icon_size: 64, ...config, }; this._updateContent(); } _updateContent() { if (!this._config || !this._hass) { return; } if (!this.content) { this.innerHTML = `
`; this.content = this.querySelector('.button-container'); this.icon = this.querySelector('ha-icon'); this.nameDiv = this.querySelector('.name'); this.stateText = this.querySelector('.state-text'); // Add click handler this.querySelector('ha-card').addEventListener('click', () => this._handleClick()); // Add styles if (!this.styleSheet) { const style = document.createElement('style'); style.textContent = ` ha-card { padding: 16px; display: flex; flex-direction: column; align-items: center; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } ha-card:hover { transform: scale(1.02); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } ha-card:active { transform: scale(0.98); } .button-container { display: flex; flex-direction: column; align-items: center; gap: 8px; } .icon-container { display: flex; align-items: center; justify-content: center; width: 100%; height: auto; } ha-icon { color: var(--primary-color); transition: color 0.3s, transform 0.2s; display: inline-flex !important; align-items: center; justify-content: center; --mdc-icon-size: var(--icon-size, 64px) !important; } ha-icon svg, ha-icon ha-svg-icon, ha-icon ha-svg-icon svg { width: 100% !important; height: 100% !important; max-width: none !important; max-height: none !important; } .icon-container.on ha-icon { color: var(--paper-item-icon-active-color, #fdd835); filter: drop-shadow(0 0 8px rgba(253, 216, 53, 0.6)); } .icon-container.off ha-icon { color: var(--disabled-text-color, #9e9e9e); } .name { font-size: 14px; font-weight: 500; color: var(--primary-text-color); text-align: center; } .state-text { font-size: 12px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; } `; this.appendChild(style); this.styleSheet = style; } } const state = this._getState(); const isOn = this._isOn(); const icon = this._getIcon(); const iconSize = this._config.icon_size || 64; // Update icon this.icon.setAttribute('icon', icon); this.icon.style.setProperty('--icon-size', `${iconSize}px`); this.icon.style.width = `${iconSize}px`; this.icon.style.height = `${iconSize}px`; this.icon.style.minWidth = `${iconSize}px`; this.icon.style.minHeight = `${iconSize}px`; this.icon.style.maxWidth = `${iconSize}px`; this.icon.style.maxHeight = `${iconSize}px`; this.icon.style.setProperty('--mdc-icon-size', `${iconSize}px`); // Ensure internal SVG scales - check multiple possible structures const svgIcon = this.icon.querySelector('ha-svg-icon'); if (svgIcon) { svgIcon.style.width = `${iconSize}px`; svgIcon.style.height = `${iconSize}px`; svgIcon.style.setProperty('--mdc-icon-size', `${iconSize}px`); const svg = svgIcon.querySelector('svg'); if (svg) { svg.style.width = `${iconSize}px`; svg.style.height = `${iconSize}px`; svg.setAttribute('width', `${iconSize}`); svg.setAttribute('height', `${iconSize}`); } } const svg = this.icon.querySelector('svg'); if (svg) { svg.style.width = `${iconSize}px`; svg.style.height = `${iconSize}px`; svg.setAttribute('width', `${iconSize}`); svg.setAttribute('height', `${iconSize}`); } // Update icon container class const iconContainer = this.querySelector('.icon-container'); if (isOn) { iconContainer.classList.add('on'); iconContainer.classList.remove('off'); } else { iconContainer.classList.add('off'); iconContainer.classList.remove('on'); } // Update name if (this._config.show_name && this._config.name) { this.nameDiv.textContent = this._config.name; this.nameDiv.style.display = 'block'; } else { this.nameDiv.style.display = 'none'; } // Update state text if (this._config.show_status) { this.stateText.textContent = state; this.stateText.style.display = 'block'; } else { this.stateText.style.display = 'none'; } } _getState() { if (!this._hass || !this._config.binary_sensor_entity) { return 'unknown'; } const stateObj = this._hass.states[this._config.binary_sensor_entity]; if (!stateObj) { return 'unknown'; } return stateObj.state; } _isOn() { const state = this._getState(); return state === 'on' || state === 'true' || state === '1'; } _getIcon() { return this._config.icon || 'mdi:lightbulb'; } async _handleClick() { if (!this._hass || !this._config.button_entity) { return; } const buttonEntity = this._hass.states[this._config.button_entity]; if (!buttonEntity) { console.error(`Button entity ${this._config.button_entity} not found`); return; } // Call the button press service await this._hass.callService('button', 'press', { entity_id: this._config.button_entity, }); } getCardSize() { return 3; } static getStubConfig() { return { button_entity: 'button.example', binary_sensor_entity: 'binary_sensor.example', icon: 'mdi:lightbulb', name: 'Light', show_name: true, show_status: true, icon_size: 64, }; } } customElements.define('light-button-card', LightButtonCard); // Card editor class LightButtonCardEditor extends HTMLElement { set hass(hass) { this._hass = hass; if (this.content && this._elements) { // Update hass on existing elements this._elements.buttonPicker.hass = hass; this._elements.sensorPicker.hass = hass; } else if (this.content) { this._updateEditor(); } } setConfig(config) { this._config = { ...config }; if (!this.content) { this._updateEditor(); } else { // Update values without rebuilding this._updateEditorValues(); } } _updateEditorValues() { if (!this._elements) return; if (this._elements.buttonPicker) { this._elements.buttonPicker.value = this._config.button_entity || ''; } if (this._elements.sensorPicker) { this._elements.sensorPicker.value = this._config.binary_sensor_entity || ''; } if (this._elements.iconPicker) { this._elements.iconPicker.value = this._config.icon || 'mdi:lightbulb'; } if (this._elements.iconSizeField) { this._elements.iconSizeField.value = this._config.icon_size || 64; } if (this._elements.nameField) { this._elements.nameField.value = this._config.name || ''; } if (this._elements.showNameSwitch) { this._elements.showNameSwitch.checked = this._config.show_name !== false; } if (this._elements.showStatusSwitch) { this._elements.showStatusSwitch.checked = this._config.show_status !== false; } } _updateEditor() { if (!this._hass) { return; } if (!this.content) { this.innerHTML = `
`; this.content = this.querySelector('.card-config'); // Add styles const style = document.createElement('style'); style.textContent = ` .card-config { display: flex; flex-direction: column; gap: 16px; } `; this.appendChild(style); } // Clear and rebuild editor only if elements don't exist if (!this._elements) { this.content.innerHTML = ''; this._elements = {}; // Button Entity Picker const buttonPicker = document.createElement('ha-entity-picker'); buttonPicker.label = 'Button Entity'; buttonPicker.hass = this._hass; buttonPicker.value = this._config.button_entity || ''; buttonPicker.configValue = 'button_entity'; buttonPicker.required = true; buttonPicker.includeDomains = ['button']; buttonPicker.addEventListener('value-changed', (ev) => { this._config.button_entity = ev.detail.value; this._fireConfigChanged(); }); this.content.appendChild(buttonPicker); this._elements.buttonPicker = buttonPicker; // Binary Sensor Entity Picker const sensorPicker = document.createElement('ha-entity-picker'); sensorPicker.label = 'Binary Sensor Entity'; sensorPicker.hass = this._hass; sensorPicker.value = this._config.binary_sensor_entity || ''; sensorPicker.configValue = 'binary_sensor_entity'; sensorPicker.required = true; sensorPicker.includeDomains = ['binary_sensor']; sensorPicker.addEventListener('value-changed', (ev) => { this._config.binary_sensor_entity = ev.detail.value; this._fireConfigChanged(); }); this.content.appendChild(sensorPicker); this._elements.sensorPicker = sensorPicker; // Icon Picker const iconPicker = document.createElement('ha-icon-picker'); iconPicker.label = 'Icon'; iconPicker.value = this._config.icon || 'mdi:lightbulb'; iconPicker.configValue = 'icon'; iconPicker.addEventListener('value-changed', (ev) => { this._config.icon = ev.detail.value; this._fireConfigChanged(); }); this.content.appendChild(iconPicker); this._elements.iconPicker = iconPicker; // Icon Size field const iconSizeField = document.createElement('ha-textfield'); iconSizeField.label = 'Icon Size (pixels)'; iconSizeField.type = 'number'; iconSizeField.value = this._config.icon_size || 64; iconSizeField.configValue = 'icon_size'; iconSizeField.min = 16; iconSizeField.max = 500; iconSizeField.step = 1; iconSizeField.addEventListener('input', (ev) => { const value = parseInt(ev.target.value); if (!isNaN(value) && value >= 16) { this._config.icon_size = value; this._fireConfigChanged(); } }); this.content.appendChild(iconSizeField); this._elements.iconSizeField = iconSizeField; // Name field const nameField = document.createElement('ha-textfield'); nameField.label = 'Name (optional)'; nameField.value = this._config.name || ''; nameField.configValue = 'name'; nameField.addEventListener('input', (ev) => { this._config.name = ev.target.value; this._fireConfigChanged(); }); this.content.appendChild(nameField); this._elements.nameField = nameField; // Show Name switch const showNameContainer = document.createElement('div'); showNameContainer.style.display = 'flex'; showNameContainer.style.alignItems = 'center'; showNameContainer.style.justifyContent = 'space-between'; const showNameLabel = document.createElement('div'); showNameLabel.textContent = 'Show Name'; showNameLabel.style.flex = '1'; const showNameSwitch = document.createElement('ha-switch'); showNameSwitch.checked = this._config.show_name !== false; showNameSwitch.configValue = 'show_name'; showNameSwitch.addEventListener('change', (ev) => { this._config.show_name = ev.target.checked; this._fireConfigChanged(); }); showNameContainer.appendChild(showNameLabel); showNameContainer.appendChild(showNameSwitch); this.content.appendChild(showNameContainer); this._elements.showNameSwitch = showNameSwitch; // Show Status switch const showStatusContainer = document.createElement('div'); showStatusContainer.style.display = 'flex'; showStatusContainer.style.alignItems = 'center'; showStatusContainer.style.justifyContent = 'space-between'; const showStatusLabel = document.createElement('div'); showStatusLabel.textContent = 'Show Status'; showStatusLabel.style.flex = '1'; const showStatusSwitch = document.createElement('ha-switch'); showStatusSwitch.checked = this._config.show_status !== false; showStatusSwitch.configValue = 'show_status'; showStatusSwitch.addEventListener('change', (ev) => { this._config.show_status = ev.target.checked; this._fireConfigChanged(); }); showStatusContainer.appendChild(showStatusLabel); showStatusContainer.appendChild(showStatusSwitch); this.content.appendChild(showStatusContainer); this._elements.showStatusSwitch = showStatusSwitch; } } _fireConfigChanged() { const event = new CustomEvent('config-changed', { bubbles: true, composed: true, detail: { config: { ...this._config } } }); this.dispatchEvent(event); } } customElements.define('light-button-card-editor', LightButtonCardEditor); // Register card for card picker LightButtonCard.getConfigElement = () => { return document.createElement('light-button-card-editor'); }; window.customCards = window.customCards || []; window.customCards.push({ type: 'light-button-card', name: 'Light Button Card', description: 'A card with button toggle and binary sensor status', });