const LitElement = customElements.get('ha-panel-lovelace') ? Object.getPrototypeOf(customElements.get('ha-panel-lovelace')) : Object.getPrototypeOf(customElements.get('hc-lovelace')); const html = LitElement.prototype.html; const css = LitElement.prototype.css; const weatherIconsDay = { clear: 'clear-day', 'clear-night': 'clear-night', cloudy: 'cloudy', fog: 'fog', hail: 'hail', lightning: 'thunderstorms', 'lightning-rainy': 'thunderstorms-rain', partlycloudy: 'partly-cloudy-day', pouring: 'extreme-rain', rainy: 'rain', snowy: 'snow', 'snowy-rainy': 'sleet', sunny: 'clear-day', windy: 'wind', 'windy-variant': 'wind', exceptional: '!!', }; const weatherIconsNight = { ...weatherIconsDay, clear: 'clear-night', sunny: 'clear-night', partlycloudy: 'partly-cloudy-night', }; const windDirections = [ 'N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N', ]; const windDirectionsSet = new Set(windDirections); window.customCards = window.customCards || []; window.customCards.push({ type: 'weather-card', name: 'Weather Card', description: 'A custom weather card with animated icons.', preview: true, documentationURL: 'https://github.com/avee87/weather-card', }); const fireEvent = (node, type, detail, options) => { options = options || {}; detail = detail === null || detail === undefined ? {} : detail; const event = new Event(type, { bubbles: options.bubbles === undefined ? true : options.bubbles, cancelable: Boolean(options.cancelable), composed: options.composed === undefined ? true : options.composed, }); event.detail = detail; node.dispatchEvent(event); return event; }; function hasConfigOrEntityChanged(element, changedProps) { if (changedProps.has('_config') || changedProps.has('_forecastEvent')) { return true; } if (!changedProps.has('hass')) { return false; } const oldHass = changedProps.get('hass'); if (oldHass) { return ( oldHass.states[element._config.entity] !== element.hass.states[element._config.entity] || oldHass.states['sun.sun'] !== element.hass.states['sun.sun'] ); } return true; } class WeatherCard extends LitElement { static get properties() { return { _config: {}, _forecastEvent: {}, hass: {}, }; } static getConfigForm() { return { schema: [ { name: 'entity', required: true, selector: { entity: { domain: 'weather' } }, }, { name: 'name', selector: { text: {} }, }, { name: 'current', default: true, selector: { boolean: {} } }, { name: 'details', default: true, selector: { boolean: {} } }, { name: 'forecast', default: true, selector: { boolean: {} } }, { name: 'forecast_type', default: 'daily', selector: { select: { options: [ { value: 'hourly', label: 'Hourly' }, { value: 'daily', label: 'Daily' }, ], }, }, }, { name: 'number_of_forecasts', default: 5, selector: { number: {} } }, { name: 'show_forecast_wind_speed', default: true, selector: { boolean: {} }, }, ], }; } static getStubConfig(hass, unusedEntities, allEntities) { let entity = unusedEntities.find((eid) => eid.split('.')[0] === 'weather'); if (!entity) { entity = allEntities.find((eid) => eid.split('.')[0] === 'weather'); } return { entity }; } setConfig(config) { if (!config.entity) { throw new Error('Please define a weather entity'); } this._config = { forecast_type: 'daily', ...config }; } _needForecastSubscription() { return ( this._config && this._config.forecast !== false && this._config.forecast_type && this._config.forecast_type !== 'legacy' ); } _unsubscribeForecastEvents() { if (this._subscribed) { this._subscribed.then((unsub) => unsub()); this._subscribed = undefined; } } async _subscribeForecastEvents() { this._unsubscribeForecastEvents(); if ( !this.isConnected || !this.hass || !this._config || !this._needForecastSubscription() ) { return; } this._subscribed = this.hass.connection.subscribeMessage( (event) => { this._forecastEvent = event; }, { type: 'weather/subscribe_forecast', forecast_type: this._config.forecast_type, entity_id: this._config.entity, } ); } connectedCallback() { super.connectedCallback(); if (this.hasUpdated && this._config && this.hass) { this._subscribeForecastEvents(); } } disconnectedCallback() { super.disconnectedCallback(); this._unsubscribeForecastEvents(); } shouldUpdate(changedProps) { return hasConfigOrEntityChanged(this, changedProps); } updated(changedProps) { if (!this.hass || !this._config) { return; } if (changedProps.has('_config') || !this._subscribed) { this._subscribeForecastEvents(); } } render() { if (!this._config || !this.hass) { return html``; } this.numberElements = 0; const lang = this.hass.selectedLanguage || this.hass.language; const stateObj = this.hass.states[this._config.entity]; if (!stateObj) { return html`
Entity not available: ${this._config.entity}
`; } return html` ${this._config.current !== false ? this.renderCurrent(stateObj) : ''} ${this._config.details !== false ? this.renderDetails(stateObj, lang) : ''} ${this._config.forecast !== false ? this.renderForecast( this._forecastEvent || { forecast: stateObj.attributes.forecast, type: this._config.forecast_type, }, lang ) : ''} `; } renderCurrent(stateObj) { this.numberElements++; return html`
${stateObj.state} ${this._config.name ? html` ${this._config.name} ` : ''} ${this.getUnit('temperature') == '°F' ? Math.round(stateObj.attributes.temperature) : stateObj.attributes.temperature} ${this.getUnit('temperature')}
`; } renderDetails(stateObj, lang) { this.numberElements++; const items = []; if (stateObj.attributes.humidity != null) { items.push( this.renderItem( 'mdi:water-percent', stateObj.attributes.humidity, this.getUnit('humidity') ) ); } if (stateObj.attributes.wind_speed != null) { const windBearing = stateObj.attributes.wind_bearing; const windDirection = windBearing != null ? this.getWindDirection(windBearing) : ''; items.push( this.renderItem( 'mdi:weather-windy', windDirection + ' ' + stateObj.attributes.wind_speed, this.getUnit('wind_speed') ) ); } if (stateObj.attributes.pressure != null) { items.push( this.renderItem( 'mdi:gauge', stateObj.attributes.pressure, this.getUnit('air_pressure') ) ); } if (stateObj.attributes.visibility != null) { items.push( this.renderItem( 'mdi:weather-fog', stateObj.attributes.visibility, this.getUnit('visibility') ) ); } const sun = this.hass.states['sun.sun']; if (sun) { const next_rising = new Date( sun.attributes.next_rising ).toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit', }); const next_setting = new Date( sun.attributes.next_setting ).toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit', }); if (items.length % 2 == 1) { items.push(html`
`); } items.push(this.renderItem('mdi:weather-sunset-up', next_rising)); items.push(this.renderItem('mdi:weather-sunset-down', next_setting)); } const listItems = items.map((item) => html`
  • ${item}
  • `); return html` `; } renderItem(icon, value, unit) { return html` ${value} ${unit != null ? html` ${unit} ` : ''} `; } renderForecast(forecast, lang) { if (!forecast || !forecast.forecast || forecast.forecast.length === 0) { return html``; } this.numberElements++; const forecastPoints = forecast.forecast .slice( 0, this._config.number_of_forecasts ? this._config.number_of_forecasts : 5 ) .map((point) => this.renderForecastPoint(forecast, point, lang)); return html`
    ${forecastPoints}
    `; } renderForecastPoint(forecast, daily, lang) { const items = []; items.push( html`
    ${forecast.type === 'hourly' ? new Date(daily.datetime).toLocaleTimeString(lang, { hour: '2-digit', minute: '2-digit', }) : new Date(daily.datetime).toLocaleDateString(lang, { weekday: 'short', })}
    ` ); items.push(html` `); items.push(html`
    ${daily.temperature}${this.getUnit('temperature')}
    `); if (daily.templow !== undefined) { items.push( html`
    ${daily.templow}${this.getUnit('temperature')}
    ` ); } if ( !this._config.hide_precipitation && daily.precipitation !== undefined && daily.precipitation !== null ) { items.push( html`
    ${Math.round(daily.precipitation * 10) / 10} ${this.getUnit('precipitation')}
    ` ); } if ( !this._config.hide_precipitation && daily.precipitation_probability !== undefined && daily.precipitation_probability !== null ) { items.push(html`
    ${Math.round(daily.precipitation_probability)} ${this.getUnit('precipitation_probability')}
    `); } if (this._config.show_forecast_wind_speed && daily.wind_speed != null) { const degrees = windDirections.includes(daily.wind_bearing) ? windDirections.indexOf(daily.wind_bearing) * 22.5 : daily.wind_bearing; items.push( html`
    ${Math.round(daily.wind_speed)} ${this.getUnit('wind_speed')}
    ` ); } return html`
    ${items}
    `; } getWindDirection(windBearing) { if (windDirectionsSet.has(windBearing)) { return windBearing; } return windDirections[parseInt((windBearing + 11.25) / 22.5)]; } getWeatherIcon(condition, sun) { return `${ this._config.icons ? this._config.icons : 'https://cdn.jsdelivr.net/gh/avee87/weather-card/dist/icons_new/' }${ sun && sun.state == 'below_horizon' ? weatherIconsNight[condition] : weatherIconsDay[condition] }.svg`; } getUnit(measure) { const lengthUnit = this.hass.config.unit_system.length; const attributes = this.hass.states[this._config.entity].attributes; switch (measure) { case 'air_pressure': return ( attributes.pressure_unit ?? (lengthUnit === 'km' ? 'hPa' : 'inHg') ); case 'humidity': return '%'; case 'precipitation': return ( attributes.precipitation_unit ?? (lengthUnit === 'km' ? 'mm' : 'in') ); case 'precipitation_probability': return '%'; case 'visibility': return attributes.visibility_unit ?? lengthUnit; case 'wind_speed': return ( attributes.wind_speed_unit ?? (lengthUnit === 'km' ? 'km/h' : 'mph') ); default: return ''; } } _handleClick() { fireEvent(this, 'hass-more-info', { entityId: this._config.entity }); } getCardSize() { return 3; } static get styles() { return css` ha-card { cursor: pointer; margin: auto; overflow: hidden; padding-top: 1.3em; padding-bottom: 1.3em; padding-left: 1em; padding-right: 1em; position: relative; } .spacer { padding-top: 1em; } .clear { clear: both; } .title { position: absolute; left: 3em; font-weight: 300; font-size: 3em; color: var(--primary-text-color); } .temp { font-weight: 300; font-size: 4em; color: var(--primary-text-color); position: absolute; right: 1em; } .tempc { font-weight: 300; font-size: 1.5em; vertical-align: super; color: var(--primary-text-color); position: absolute; right: 1em; margin-top: -14px; margin-right: 7px; } @media (max-width: 460px) { .title { font-size: 2.2em; left: 4em; } .temp { font-size: 3em; } .tempc { font-size: 1em; } } .current { padding: 1.2em 0; margin-bottom: 3.5em; } .variations { display: flex; flex-flow: row wrap; justify-content: space-between; font-weight: 300; color: var(--primary-text-color); list-style: none; padding: 0 1em; margin: 0; } .variations ha-icon { height: 22px; margin-right: 5px; color: var(--paper-item-icon-color); } .variations li { flex-basis: auto; width: 50%; } .variations li:nth-child(2n) { text-align: right; } .variations li:nth-child(2n) ha-icon { margin-right: 0; margin-left: 8px; float: right; } .unit { font-size: 0.8em; } .forecast { width: 100%; margin: 0 auto; display: flex; } .day { flex: 1; display: block; text-align: center; color: var(--primary-text-color); border-right: 0.1em solid #d9d9d9; line-height: 2; box-sizing: border-box; } .dayname { text-transform: uppercase; } .forecast .day:first-child { margin-left: 0; } .forecast .day:nth-last-child(1) { border-right: none; margin-right: 0; } .highTemp { font-weight: bold; } .lowTemp { color: var(--secondary-text-color); } .wind, .precipitation { color: var(--primary-text-color); font-weight: 300; } .icon.bigger { width: 10em; height: 10em; margin-top: -4em; position: absolute; left: 0em; } .icon { width: 50px; height: 50px; margin-right: 5px; display: inline-block; vertical-align: middle; background-size: contain; background-position: center center; background-repeat: no-repeat; text-indent: -9999px; } .weather { font-weight: 300; font-size: 1.5em; color: var(--primary-text-color); text-align: left; position: absolute; top: -0.5em; left: 6em; word-wrap: break-word; width: 30%; } .windspeed { white-space: nowrap; } .wind-direction-arrow { display: inline-block; } `; } } customElements.define('weather-card', WeatherCard);