Add HACS integration, 8 Lovelace cards, and pairing page
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>
This commit is contained in:
1 parent
a890ba6baf
commit
d681f20884
18 files changed
+2708
No files matched your search
@@ -0,0 +1,106 @@
|
||||
const LUMA_MEDIA_VERSION = '0.1.0';
|
||||
|
||||
class LumaMediaCard 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 || '',
|
||||
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]; }
|
||||
|
||||
_isActive() {
|
||||
const s = this._getState();
|
||||
return s && (s.state === 'playing' || s.state === 'paused');
|
||||
}
|
||||
|
||||
_getAccent() {
|
||||
if (this._config?.accent_color) return this._config.accent_color;
|
||||
const s = this._getState();
|
||||
if (!s) return '#bbbbbb';
|
||||
switch (s.state) {
|
||||
case 'playing': return '#9b7ddb';
|
||||
case 'paused': return 'rgba(125,155,219,0.7)';
|
||||
default: return 'rgba(187,187,187,0.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 s = this._getState();
|
||||
const isActive = this._isActive();
|
||||
const accent = this._getAccent();
|
||||
const icon = this._config.icon || 'mdi:music';
|
||||
const title = s?.attributes?.media_title || this._config.name || s?.attributes?.friendly_name || '';
|
||||
const artist = s?.attributes?.media_artist || '';
|
||||
const volume = Math.round((s?.attributes?.volume_level || 0) * 100);
|
||||
const state = s?.state || 'off';
|
||||
const statusLabel = { playing: 'Playing', paused: 'Paused', idle: 'Idle' }[state] || 'Off';
|
||||
const volLabel = isActive ? `Vol ${volume}%` : 'Off';
|
||||
|
||||
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: 240px; display: flex; flex-direction: column;
|
||||
align-items: center; 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.active { border-color: color-mix(in srgb, var(--accent) 25%, transparent); }
|
||||
.glow {
|
||||
position: absolute; top: 0; left: 50%; transform: translateX(-50%);
|
||||
width: 200px; height: 200px; border-radius: 50%; pointer-events: none;
|
||||
opacity: 0; transition: opacity .2s;
|
||||
background: radial-gradient(circle, rgba(128,96,192,0.25) 0%, transparent 70%);
|
||||
}
|
||||
.card.active .glow { opacity: 1; }
|
||||
.art {
|
||||
width: 120px; height: 120px; border-radius: 60px; margin-top: 8px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: ${isActive ? 'color-mix(in srgb, var(--accent) 25%, transparent)' : 'rgba(255,255,255,0.08)'};
|
||||
position: relative; z-index: 1;
|
||||
}
|
||||
.art ha-icon { --mdc-icon-size: 60px; color: ${isActive ? 'var(--accent)' : 'rgba(187,187,187,0.4)'}; }
|
||||
.text-area { text-align: center; margin-top: 16px; width: 100%; padding: 0 8px; position: relative; z-index: 1; flex: 1; }
|
||||
.title { font-size: 17px; font-weight: 600; letter-spacing: -0.02em; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.artist { font-size: 13px; font-weight: 400; color: rgba(232,232,232,0.6); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.state-label { font-size: 11px; font-weight: 400; letter-spacing: 0.04em; color: ${isActive ? 'var(--accent)' : 'rgba(232,232,232,0.45)'}; margin-top: 8px; }
|
||||
.vol { font-size: 13px; font-weight: 500; color: ${isActive ? '#fff' : 'rgba(187,187,187,0.5)'}; text-align: center; padding-bottom: 4px; position: relative; z-index: 1; }
|
||||
</style>
|
||||
<div class="card ${isActive ? 'active' : ''}">
|
||||
<div class="glow"></div>
|
||||
<div class="art"><ha-icon icon="${icon}"></ha-icon></div>
|
||||
<div class="text-area">
|
||||
<div class="title">${title}</div>
|
||||
${artist ? `<div class="artist">${artist}</div>` : ''}
|
||||
<div class="state-label">${statusLabel}</div>
|
||||
</div>
|
||||
<div class="vol">${volLabel}</div>
|
||||
</div>`;
|
||||
this.shadowRoot.querySelector('.card').addEventListener('click', () => this._handleClick());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('luma-media-card', LumaMediaCard);
|
||||
window.customCards = window.customCards || [];
|
||||
window.customCards.push({ type: 'luma-media-card', name: 'Luma Media', description: 'Luma-style media player card with album art placeholder.', preview: true });
|
||||
console.info(`%c LUMA-MEDIA-CARD %c v${LUMA_MEDIA_VERSION} `, 'background:#9b7ddb;color:#111;font-weight:bold', 'background:#161a22;color:#9b7ddb');
|
||||
Reference in new issue
Block a user