Add HA Lovelace cards replicating Luma glass UI
CI / Identify a merged version PR (push) Canceled after 0s
CI / Rust checks, rendering, and native package (push) Canceled after 0s
CI / Publish release and Homebrew feed (push) Canceled after 0s
Release PR / Maintain Version Packages PR (push) Canceled after 0s

Custom card bundle (luma-card, luma-weather-card) with slide-in
panels for light/climate/cover/media/vacuum, matching the TV
launcher's design tokens. Includes theme, dashboard YAML for all
8 rooms, and deployment handoff doc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Kaloyan DanchevandClaude Opus 4.6 committed 2026-10-27 23:11:43 +02:00
1 parent bde779d759
commit 89ff17a3bc
6 files changed
+1766

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
/**
* Luma Weather Card — custom Lovelace card
* Shows current conditions + 5-day forecast in the Luma glass style.
*/
const WEATHER_VERSION = '1.0.0';
const WEATHER_ICONS = {
'clear-night': 'mdi:weather-night',
'cloudy': 'mdi:weather-cloudy',
'fog': 'mdi:weather-fog',
'hail': 'mdi:weather-hail',
'lightning': 'mdi:weather-lightning',
'lightning-rainy': 'mdi:weather-lightning-rainy',
'partlycloudy': 'mdi:weather-partly-cloudy',
'pouring': 'mdi:weather-pouring',
'rainy': 'mdi:weather-rainy',
'snowy': 'mdi:weather-snowy',
'snowy-rainy': 'mdi:weather-snowy-rainy',
'sunny': 'mdi:weather-sunny',
'windy': 'mdi:weather-windy',
'windy-variant': 'mdi:weather-windy-variant',
'exceptional': 'mdi:alert-circle-outline',
};
const CONDITION_LABELS = {
'clear-night': 'Clear Night',
'cloudy': 'Cloudy',
'fog': 'Foggy',
'hail': 'Hail',
'lightning': 'Thunderstorm',
'lightning-rainy': 'Thunderstorm',
'partlycloudy': 'Partly Cloudy',
'pouring': 'Heavy Rain',
'rainy': 'Rainy',
'snowy': 'Snowy',
'snowy-rainy': 'Sleet',
'sunny': 'Sunny',
'windy': 'Windy',
'windy-variant': 'Windy',
'exceptional': 'Exceptional',
};
class LumaWeatherCard extends HTMLElement {
set hass(hass) {
this._hass = hass;
if (!this._config) return;
this._render();
}
setConfig(config) {
if (!config.entity) throw new Error('Missing entity');
this._config = config;
}
_render() {
const hass = this._hass;
const config = this._config;
const stateObj = hass.states[config.entity];
if (!stateObj) {
this.innerHTML = `<ha-card><div style="padding:16px;color:#888">Entity not found: ${config.entity}</div></ha-card>`;
return;
}
const condition = stateObj.state;
const temp = stateObj.attributes.temperature;
const unit = stateObj.attributes.temperature_unit || '°C';
const humidity = stateObj.attributes.humidity;
const windSpeed = stateObj.attributes.wind_speed;
const windUnit = stateObj.attributes.wind_speed_unit || 'km/h';
const icon = WEATHER_ICONS[condition] || 'mdi:weather-partly-cloudy';
const label = CONDITION_LABELS[condition] || condition;
const forecast = stateObj.attributes.forecast || [];
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = WEATHER_STYLES;
this.shadowRoot.appendChild(style);
const wrap = document.createElement('div');
wrap.className = 'lw-wrap';
this.shadowRoot.appendChild(wrap);
}
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const forecastDays = forecast.slice(0, 5);
this.shadowRoot.querySelector('.lw-wrap').innerHTML = `
<div class="lw-card">
<div class="lw-now">
<div class="lw-icon-wrap">
<ha-icon icon="${icon}" style="--mdc-icon-size:56px;color:#F5B85A"></ha-icon>
</div>
<div class="lw-now-info">
<div class="lw-temp">${temp !== undefined ? Math.round(temp) : '–'}°</div>
<div class="lw-condition">${label}</div>
</div>
</div>
<div class="lw-details">
<div class="lw-detail">
<ha-icon icon="mdi:water-percent" style="--mdc-icon-size:20px;color:rgba(255,255,255,0.60)"></ha-icon>
<span>${humidity !== undefined ? humidity + '%' : '–'}</span>
</div>
<div class="lw-detail">
<ha-icon icon="mdi:weather-windy" style="--mdc-icon-size:20px;color:rgba(255,255,255,0.60)"></ha-icon>
<span>${windSpeed !== undefined ? windSpeed + ' ' + windUnit : '–'}</span>
</div>
</div>
${forecastDays.length > 0 ? `
<div class="lw-divider"></div>
<div class="lw-forecast">
${forecastDays.map(f => {
const d = new Date(f.datetime);
const day = days[d.getDay()];
const fIcon = WEATHER_ICONS[f.condition] || 'mdi:weather-partly-cloudy';
const lo = f.templow !== undefined ? Math.round(f.templow) : '–';
const hi = f.temperature !== undefined ? Math.round(f.temperature) : '–';
return `
<div class="lw-day">
<div class="lw-day-name">${day}</div>
<ha-icon icon="${fIcon}" style="--mdc-icon-size:24px;color:rgba(255,255,255,0.72)"></ha-icon>
<div class="lw-day-range">
<span class="lw-hi">${hi}°</span>
<span class="lw-lo">${lo}°</span>
</div>
</div>
`;
}).join('')}
</div>
` : ''}
</div>
`;
}
getCardSize() { return 4; }
static getStubConfig() { return { entity: 'weather.forecast_home' }; }
}
const WEATHER_STYLES = `
:host { display: block; }
.lw-card {
background: rgba(34,38,46,0.88);
border: 2px solid rgba(255,255,255,0.10);
border-radius: 22px;
padding: 22px;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
box-sizing: border-box;
}
.lw-now {
display: flex; align-items: center; gap: 16px;
margin-bottom: 16px;
}
.lw-icon-wrap {
width: 72px; height: 72px; border-radius: 50%;
background: rgba(245,184,90,0.18);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.lw-now-info { flex: 1; }
.lw-temp {
font-size: 48px; font-weight: 700; color: #fff;
line-height: 1; letter-spacing: -0.03em;
}
.lw-condition {
font-size: 17px; font-weight: 500; color: rgba(255,255,255,0.72);
margin-top: 4px;
}
.lw-details {
display: flex; gap: 20px; margin-bottom: 16px;
}
.lw-detail {
display: flex; align-items: center; gap: 6px;
font-size: 15px; font-weight: 500; color: rgba(255,255,255,0.72);
}
.lw-divider {
height: 1px; background: rgba(255,255,255,0.10);
margin-bottom: 16px;
}
.lw-forecast {
display: flex; justify-content: space-between;
}
.lw-day {
display: flex; flex-direction: column; align-items: center; gap: 6px;
flex: 1;
}
.lw-day-name {
font-size: 13px; font-weight: 600; color: rgba(255,255,255,0.60);
text-transform: uppercase; letter-spacing: 0.04em;
}
.lw-day-range {
display: flex; flex-direction: column; align-items: center; gap: 2px;
}
.lw-hi { font-size: 16px; font-weight: 700; color: #fff; }
.lw-lo { font-size: 13px; font-weight: 500; color: rgba(255,255,255,0.50); }
`;
customElements.define('luma-weather-card', LumaWeatherCard);
window.customCards = window.customCards || [];
window.customCards.push({
type: 'luma-weather-card',
name: 'Luma Weather Card',
description: 'Weather card with 5-day forecast in the Luma glass design.',
preview: true,
});
console.info(`%c LUMA-WEATHER %c v${WEATHER_VERSION} `, 'background:#F5B85A;color:#15181E;font-weight:700', 'background:#22262E;color:#F5B85A');