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>
208 lines
6.8 KiB
JavaScript
208 lines
6.8 KiB
JavaScript
/**
|
||
* 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');
|