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 `
${dayLabel}
${high}
${low}
`; }).join('') : ''; const hasForecast = forecastHtml.length > 0; this.shadowRoot.innerHTML = `
${city}
${typeof temp === 'number' ? Math.round(temp) : temp}°
${conditionLabel}
${typeof humidity === 'number' ? Math.round(humidity) : humidity}%
${typeof wind === 'number' ? Math.round(wind) : wind} ${windUnit}
${typeof temp === 'number' ? Math.round(temp) : temp}°C
${hasForecast ? `
${forecastHtml}
` : ''}
`; 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');