Panel fixes: tall card row_span, room tab styling, vacuum card image+value, AC airflow labels, AC fan speed sections (auto 3-btn + manual 5-step slider), light subtitle fallback, curtain pane dividers, dimmable light presets, LED panel brightness + reorder. New assets: 34 ui-icons (climate modes, cover, effects, battery, led-strip, switch states), 3 logos, 90 app surface PNGs, design system specs (components, ux-mechanics, tokens). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
193 lines
8.7 KiB
JavaScript
193 lines
8.7 KiB
JavaScript
const LUMA_WEATHER_VERSION = '0.2.1';
|
|
|
|
class LumaWeatherCard extends HTMLElement {
|
|
set hass(hass) {
|
|
const changed = this._hass !== hass;
|
|
this._hass = hass;
|
|
if (changed) this._fetchForecast();
|
|
this._render();
|
|
}
|
|
|
|
setConfig(config) {
|
|
if (!config.entity) throw new Error('Entity is required');
|
|
this._config = { entity: config.entity, name: config.name || '', size: config.size || '2x2',
|
|
icon: config.icon || '', tap_action: config.tap_action || { action: 'more-info' },
|
|
hold_action: config.hold_action || { action: 'more-info' }, ...config };
|
|
this._forecast = [];
|
|
this._forecastFetched = false;
|
|
this._render();
|
|
}
|
|
|
|
getCardSize() { return 4; }
|
|
static getStubConfig() { return { entity: '', name: '', size: '2x2' }; }
|
|
connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._render(); }
|
|
|
|
_getState() { return this._hass?.states?.[this._config?.entity]; }
|
|
|
|
async _fetchForecast() {
|
|
if (!this._hass || !this._config || this._forecastFetched) return;
|
|
this._forecastFetched = true;
|
|
try {
|
|
const resp = await this._hass.callWS({
|
|
type: 'weather/subscribe_forecast',
|
|
entity_id: this._config.entity,
|
|
forecast_type: 'daily',
|
|
});
|
|
} catch {
|
|
try {
|
|
const result = await this._hass.callService('weather', 'get_forecasts', {
|
|
entity_id: this._config.entity, type: 'daily',
|
|
}, {}, false, true);
|
|
const data = result?.response?.[this._config.entity]?.forecast || [];
|
|
this._forecast = data.slice(0, 5);
|
|
this._render();
|
|
} catch { /* forecast not available */ }
|
|
}
|
|
}
|
|
|
|
_getWeatherIcon(condition) {
|
|
const map = {
|
|
'sunny': 'mdi:weather-sunny', 'clear-night': 'mdi:weather-night',
|
|
'cloudy': 'mdi:weather-cloudy', 'fog': 'mdi:weather-fog',
|
|
'partlycloudy': 'mdi:weather-partly-cloudy',
|
|
'rainy': 'mdi:weather-rainy', 'pouring': 'mdi:weather-pouring',
|
|
'snowy': 'mdi:weather-snowy', 'lightning': 'mdi:weather-lightning',
|
|
'lightning-rainy': 'mdi:weather-lightning-rainy', 'hail': 'mdi:weather-hail',
|
|
'windy': 'mdi:weather-windy', 'windy-variant': 'mdi:weather-windy-variant',
|
|
'exceptional': 'mdi:alert-circle',
|
|
};
|
|
return map[condition] || 'mdi:weather-sunny';
|
|
}
|
|
|
|
_conditionLabel(condition) {
|
|
const map = {
|
|
'sunny': 'Sunny', 'clear-night': 'Clear', 'cloudy': 'Cloudy',
|
|
'partlycloudy': 'Partly Cloudy', 'fog': 'Fog', 'rainy': 'Rain',
|
|
'pouring': 'Heavy Rain', 'snowy': 'Snow', 'lightning': 'Thunderstorm',
|
|
'lightning-rainy': 'Thunderstorm', 'hail': 'Hail',
|
|
'windy': 'Windy', 'windy-variant': 'Windy', 'exceptional': 'Exceptional',
|
|
};
|
|
return map[condition] || 'Unknown';
|
|
}
|
|
|
|
_dayAbbr(dateStr) {
|
|
try {
|
|
const d = new Date(dateStr + 'T12:00:00');
|
|
return ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()] || '?';
|
|
} catch { return '?'; }
|
|
}
|
|
|
|
_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 condition = s?.state || 'sunny';
|
|
const temp = s?.attributes?.temperature ?? '--';
|
|
const humidity = s?.attributes?.humidity ?? '--';
|
|
const wind = s?.attributes?.wind_speed ?? '--';
|
|
const windUnit = s?.attributes?.wind_speed_unit || 'km/h';
|
|
const icon = this._config.icon || this._getWeatherIcon(condition);
|
|
const conditionLabel = this._conditionLabel(condition);
|
|
const city = this._config.name || s?.attributes?.friendly_name || '';
|
|
|
|
const forecast = s?.attributes?.forecast || this._forecast || [];
|
|
const days = forecast.slice(0, 5);
|
|
const forecastHtml = days.length > 0 ? days.map(day => {
|
|
const dateStr = (day.datetime || '').split('T')[0];
|
|
const dayLabel = this._dayAbbr(dateStr);
|
|
const dayIcon = this._getWeatherIcon(day.condition || condition);
|
|
const high = day.temperature != null ? `${Math.round(day.temperature)}°` : '';
|
|
const low = day.templow != null ? `${Math.round(day.templow)}°` : '';
|
|
return `<div class="fc-day">
|
|
<div class="fc-label">${dayLabel}</div>
|
|
<ha-icon icon="${dayIcon}" class="fc-icon"></ha-icon>
|
|
<div class="fc-high">${high}</div>
|
|
<div class="fc-low">${low}</div>
|
|
</div>`;
|
|
}).join('') : '';
|
|
|
|
const hasForecast = forecastHtml.length > 0;
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
:host { display: block; }
|
|
.card {
|
|
position: relative; overflow: hidden; background: rgba(36,40,48,0.75);
|
|
border: 1px solid rgba(255,255,255,0.10); border-radius: 22px;
|
|
cursor: pointer; transition: border-color .15s, box-shadow .15s;
|
|
height: 100%; min-height: ${hasForecast ? '280' : '200'}px; padding: 20px;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,.06);
|
|
display: flex; flex-direction: column;
|
|
}
|
|
.card:hover { border-color: rgba(255,255,255,0.18); box-shadow: 0 8px 28px rgba(0,0,0,.28); }
|
|
|
|
.layout { display: flex; flex: 1; }
|
|
.left { flex: 1; display: flex; flex-direction: column; }
|
|
.right { flex: 1; display: flex; align-items: flex-start; justify-content: center; padding-top: 10px; }
|
|
|
|
.city { display: flex; align-items: center; gap: 6px; }
|
|
.city ha-icon { --mdc-icon-size: 14px; color: rgba(160,192,224,0.7); }
|
|
.city-name { font-size: 13px; font-weight: 500; letter-spacing: 0.02em; color: rgba(160,192,224,0.7); }
|
|
|
|
.temp { font-size: 52px; font-weight: 600; letter-spacing: -0.04em; color: #fff; margin-top: 4px; line-height: 1; }
|
|
.condition { font-size: 14px; font-weight: 400; color: rgba(232,232,232,0.55); margin-top: 6px; }
|
|
|
|
.details { margin-top: 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
.detail-row { display: flex; align-items: center; gap: 6px; }
|
|
.detail-row ha-icon { --mdc-icon-size: 16px; color: rgba(96,176,240,0.7); }
|
|
.detail-value { font-size: 13px; font-weight: 400; color: rgba(232,232,232,0.55); }
|
|
|
|
.right ha-icon { --mdc-icon-size: 80px; color: #f0d080; }
|
|
.temp-unit { font-size: 14px; font-weight: 400; color: rgba(232,232,232,0.45); text-align: center; margin-top: 8px; }
|
|
|
|
.forecast-sep { height: 1px; background: rgba(255,255,255,0.08); margin: 0; }
|
|
.forecast { display: flex; justify-content: space-around; padding-top: 10px; }
|
|
.fc-day { text-align: center; flex: 1; }
|
|
.fc-label { font-size: 11px; font-weight: 500; letter-spacing: 0.02em; color: rgba(232,232,232,0.5); }
|
|
.fc-icon { --mdc-icon-size: 20px; color: rgba(240,208,128,0.7); display: block; margin: 4px auto; }
|
|
.fc-high { font-size: 12px; font-weight: 600; color: #fff; margin-top: 4px; }
|
|
.fc-low { font-size: 11px; font-weight: 400; color: rgba(232,232,232,0.4); margin-top: 2px; }
|
|
</style>
|
|
<div class="card">
|
|
<div class="layout">
|
|
<div class="left">
|
|
<div class="city">
|
|
<ha-icon icon="mdi:map-marker"></ha-icon>
|
|
<span class="city-name">${city}</span>
|
|
</div>
|
|
<div class="temp">${typeof temp === 'number' ? Math.round(temp) : temp}°</div>
|
|
<div class="condition">${conditionLabel}</div>
|
|
<div class="details">
|
|
<div class="detail-row">
|
|
<ha-icon icon="mdi:water-percent"></ha-icon>
|
|
<span class="detail-value">${typeof humidity === 'number' ? Math.round(humidity) : humidity}%</span>
|
|
</div>
|
|
<div class="detail-row">
|
|
<span class="detail-value">${typeof wind === 'number' ? Math.round(wind) : wind} ${windUnit}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="right">
|
|
<div>
|
|
<ha-icon icon="${icon}"></ha-icon>
|
|
<div class="temp-unit">${typeof temp === 'number' ? Math.round(temp) : temp}°C</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
${hasForecast ? `<div class="forecast-sep"></div><div class="forecast">${forecastHtml}</div>` : ''}
|
|
</div>`;
|
|
this.shadowRoot.querySelector('.card').addEventListener('click', () => this._handleClick());
|
|
}
|
|
}
|
|
|
|
customElements.define('luma-weather-card', LumaWeatherCard);
|
|
window.customCards = window.customCards || [];
|
|
window.customCards.push({ type: 'luma-weather-card', name: 'Luma Weather', description: 'Luma-style weather card with city, forecast, and conditions — matches TV layout.', preview: true });
|
|
console.info(`%c LUMA-WEATHER-CARD %c v${LUMA_WEATHER_VERSION} `, 'background:#f0d080;color:#111;font-weight:bold', 'background:#161a22;color:#f0d080');
|