New HA integration (integration/custom_components/luma_home/) with: - WS API: luma/get_config (Lovelace→v2 bridge), luma/pair, luma/pair_status - Config flow with dashboard URL path setting 8 HACS frontend cards (cards/) matching TV app visual spec: - toggle, range, climate, media, curtain, action, vacuum, weather - Glass card design: rgba(36,40,48,0.75) bg, 22px radius, accent glows Pairing page (pairing/luma-pair.html) for TV token provisioning. Web dashboard (web-dashboard/) — legacy config editor (deprecated). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
101 lines
4.9 KiB
JavaScript
101 lines
4.9 KiB
JavaScript
const LUMA_CURTAIN_VERSION = '0.1.0';
|
|
|
|
class LumaCurtainCard 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 || '1x1',
|
|
accent_color: config.accent_color || '', icon: config.icon || '',
|
|
tap_action: config.tap_action || { action: 'more-info' },
|
|
hold_action: config.hold_action || { action: 'more-info' }, ...config };
|
|
this._render();
|
|
}
|
|
|
|
getCardSize() { return 2; }
|
|
static getStubConfig() { return { entity: '', name: '', size: '1x1' }; }
|
|
connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); }
|
|
|
|
_getState() { return this._hass?.states?.[this._config?.entity]; }
|
|
|
|
_getPosition() {
|
|
const s = this._getState();
|
|
return parseFloat(s?.attributes?.current_position) || 0;
|
|
}
|
|
|
|
_isOpen() { return this._getPosition() > 5; }
|
|
|
|
_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 pos = this._getPosition();
|
|
const isOpen = this._isOpen();
|
|
const name = this._config.name || this._getState()?.attributes?.friendly_name || '';
|
|
const icon = this._config.icon || 'mdi:curtains';
|
|
const accent = isOpen ? '#dcd2b4' : 'rgba(187,187,187,0.5)';
|
|
const label = pos > 0 ? `${Math.round(pos)}%` : 'Closed';
|
|
const closedPct = 1 - Math.min(pos / 100, 1);
|
|
const closedOverlay = closedPct > 0.05 ? `rgba(12,16,36,${(0.5 * closedPct).toFixed(2)})` : 'transparent';
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
:host { display: block; --accent: ${accent}; }
|
|
.card {
|
|
position: relative; overflow: hidden; background: rgba(36,40,48,0.75);
|
|
border: 1px solid rgba(255,255,255,0.10); border-radius: 22px; padding: 16px;
|
|
cursor: pointer; transition: border-color .15s, box-shadow .15s;
|
|
height: 100%; min-height: 120px; display: flex; flex-direction: column;
|
|
justify-content: space-between; box-shadow: 0 2px 6px rgba(0,0,0,.06);
|
|
}
|
|
.card:hover { border-color: rgba(255,255,255,0.18); box-shadow: 0 8px 28px rgba(0,0,0,.28); }
|
|
.card.open { border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
|
|
.closed-overlay {
|
|
position: absolute; inset: 0; border-radius: 22px;
|
|
background: ${closedOverlay}; pointer-events: none; z-index: 0;
|
|
}
|
|
.glow {
|
|
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
|
|
width: 200px; height: 200px; border-radius: 50%; pointer-events: none;
|
|
opacity: 0; transition: opacity .2s;
|
|
background: radial-gradient(circle, rgba(220,210,180,0.20) 0%, transparent 70%);
|
|
}
|
|
.card.open .glow { opacity: 1; }
|
|
.top { display: flex; align-items: center; gap: 12px; position: relative; z-index: 1; }
|
|
.icon-circle {
|
|
width: 48px; height: 48px; border-radius: 24px; display: flex;
|
|
align-items: center; justify-content: center; flex-shrink: 0;
|
|
background: ${isOpen ? 'color-mix(in srgb, var(--accent) 20%, transparent)' : 'rgba(255,255,255,0.08)'};
|
|
}
|
|
.icon-circle ha-icon { --mdc-icon-size: 28px; color: var(--accent); }
|
|
.info { flex: 1; min-width: 0; }
|
|
.type-label { font-size: 12px; font-weight: 400; letter-spacing: 0.04em; color: ${isOpen ? 'var(--accent)' : 'rgba(232,232,232,0.45)'}; }
|
|
.name { font-size: 16px; font-weight: 600; letter-spacing: -0.02em; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
|
|
.status { font-size: 20px; font-weight: 700; letter-spacing: -0.02em; color: ${isOpen ? '#fff' : 'rgba(187,187,187,0.5)'}; position: relative; z-index: 1; }
|
|
</style>
|
|
<div class="card ${isOpen ? 'open' : ''}">
|
|
<div class="closed-overlay"></div>
|
|
<div class="glow"></div>
|
|
<div class="top">
|
|
<div class="icon-circle"><ha-icon icon="${icon}"></ha-icon></div>
|
|
<div class="info">
|
|
<div class="type-label">Curtain</div>
|
|
<div class="name">${name}</div>
|
|
</div>
|
|
</div>
|
|
<div class="status">${label}</div>
|
|
</div>`;
|
|
this.shadowRoot.querySelector('.card').addEventListener('click', () => this._handleClick());
|
|
}
|
|
}
|
|
|
|
customElements.define('luma-curtain-card', LumaCurtainCard);
|
|
window.customCards = window.customCards || [];
|
|
window.customCards.push({ type: 'luma-curtain-card', name: 'Luma Curtain', description: 'Luma-style cover/curtain card with position display.', preview: true });
|
|
console.info(`%c LUMA-CURTAIN-CARD %c v${LUMA_CURTAIN_VERSION} `, 'background:#dcd2b4;color:#111;font-weight:bold', 'background:#161a22;color:#dcd2b4');
|