Files
luma-home/ha-cards/luma-weather-card.js
Kaloyan DanchevandClaude Opus 4.6 89ff17a3bc
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
Add HA Lovelace cards replicating Luma glass UI
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>
2026-10-27 23:11:43 +02:00

208 lines
6.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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');