const LitElement = Object.getPrototypeOf(customElements.get("hui-view")); const html = LitElement.prototype.html; const locale = { da: { temp: "Temperatur", tempHi: "Temperatur maks", tempLo: "Temperatur nat", precip: "Nedbør", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NØ', 'NØ', 'Ø-NØ', 'Ø', 'Ø-SØ', 'SØ', 'S-SØ', 'S', 'S-SV', 'SV', 'V-SV', 'V', 'V-NV', 'NV', 'N-NV', 'N' ] }, de: { temp: "temperatur", tempHi: "Höchsttemperatur", tempLo: "Tiefsttemperatur", precip: "Niederschlag", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NO', 'NO', 'O-NO', 'O', 'O-SO', 'SO', 'S-SO', 'S', 'S-SW', 'SW', 'W-SW', 'W', 'W-NW', 'NW', 'N-NW', 'N' ] }, en: { temp: "Temperature", tempHi: "Temperature max", tempLo: "Temperature min", precip: "Precipitations", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NE', 'NE', 'E-NE', 'E', 'E-SE', 'SE', 'S-SE', 'S', 'S-SW', 'SW', 'W-SW', 'W', 'W-NW', 'NW', 'N-NW', 'N' ] }, es: { temp: "Temperatura", tempHi: "Temperatura máxima", tempLo: "Temperatura mínima", precip: "Precipitations", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NE', 'NE', 'E-NE', 'E', 'E-SE', 'SE', 'S-SE', 'S', 'S-SO', 'SO', 'O-SO', 'O', 'O-NO', 'NO', 'N-NO', 'N' ] }, fr: { temp: "Température", tempHi: "Température max", tempLo: "Température min", precip: "Précipitations", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NE', 'NE', 'E-NE', 'E', 'E-SE', 'SE', 'S-SE', 'S', 'S-SO', 'SO', 'O-SO', 'O', 'O-NO', 'NO', 'N-NO', 'N' ] }, nl: { temp: "temperatuur", tempHi: "Maximum temperatuur", tempLo: "Minimum temperatuur", precip: "Neerslag", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NO', 'NO', 'O-NO', 'O', 'O-ZO', 'ZO', 'Z-ZO', 'Z', 'Z-ZW', 'ZW', 'W-ZW', 'W', 'W-NW', 'NW', 'N-NW', 'N' ] }, ru: { temp: "Температура", tempHi: "Температура макси", tempLo: "Температура ночью", precip: "Осадки", uPress: "гПа", uSpeed: "м/с", uPrecip: "мм", cardinalDirections: [ 'С', 'С-СВ', 'СВ', 'В-СВ', 'В', 'В-ЮВ', 'ЮВ', 'Ю-ЮВ', 'Ю', 'Ю-ЮЗ', 'ЮЗ', 'З-ЮЗ', 'З', 'З-СЗ', 'СЗ', 'С-СЗ', 'С' ] }, sv: { temp: "Temperatur", tempHi: "Max temperatur", tempLo: "Min temperatur", precip: "Nederbörd", uPress: "hPa", uSpeed: "m/s", uPrecip: "mm", cardinalDirections: [ 'N', 'N-NO', 'NO', 'O-NO', 'O', 'O-SO', 'SO', 'S-SO', 'S', 'S-SV', 'SV', 'V-SV', 'V', 'V-NV', 'NV', 'N-NV', 'N' ] } }; const cardinalDirectionsIcon = [ 'mdi:arrow-down', 'mdi:arrow-bottom-left', 'mdi:arrow-left', 'mdi:arrow-top-left', 'mdi:arrow-up', 'mdi:arrow-top-right', 'mdi:arrow-right', 'mdi:arrow-bottom-right', 'mdi:arrow-down' ]; const weatherIconsDay = { clear: "day", "clear-night": "night", cloudy: "cloudy", fog: "cloudy", hail: "rainy-7", lightning: "thunder", "lightning-rainy": "thunder", partlycloudy: "cloudy-day-3", pouring: "rainy-6", rainy: "rainy-5", snowy: "snowy-6", "snowy-rainy": "rainy-7", sunny: "day", windy: "cloudy", "windy-variant": "cloudy-day-3", exceptional: "!!" }; const weatherIconsNight = { ...weatherIconsDay, clear: "night", sunny: "night", partlycloudy: "cloudy-night-3", "windy-variant": "cloudy-night-3" }; 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")) { return true; } 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: {}, hass: {} }; } static async getConfigElement() { await import("./weather-card-editor.js"); return document.createElement("weather-card-editor"); } static getStubConfig() { return {}; } setConfig(config) { if (!config.entity) { throw new Error("Please define a weather entity"); } this._config = config; if (this._config.forecast_max_Column) this._config.forecast_max_Column = parseInt(this._config.forecast_max_Column); this.setWeatherObj(); } setWeatherObj() { if (!this.hass) return; this.weatherObj = this._config.entity in this.hass.states ? this.hass.states[this._config.entity] : null; if (!this.weatherObj) return; if (!this._config.forecast_max_Column || this._config.forecast_max_Column < 2) this.forecast = this.weatherObj.attributes.forecast.slice(0,9); else this.forecast = this.weatherObj.attributes.forecast.slice(0,this._config.forecast_max_Column); //determine if the weather is hourly or daily var diffHours = Math.abs(new Date(this.forecast[0].datetime) - new Date(this.forecast[1].datetime)) / 36e5; if (diffHours === 1) this.mode = 'hourly'; else this.mode = 'daily'; } shouldUpdate(changedProps) { return hasConfigOrEntityChanged(this, changedProps); } updated(param) { this.setWeatherObj(); var chart = this.shadowRoot.getElementById("Chart"); if (chart) chart.data = this.ChartData; } render() { if (!this._config || !this.hass) { return html``; } this.setWeatherObj(); this.numberElements = 0; this.lang = this.hass.selectedLanguage || this.hass.language; if (!this.weatherObj) { return html`
Entity not available: ${this._config.entity}
`; } return html` ${this.renderStyle()} ${this._config.current !== false ? this.renderCurrent() : ""} ${this._config.details !== false ? this.renderDetails() : ""} ${this._config.forecast !== false ? this.renderForecast() : ""} `; } renderCurrent() { this.numberElements++; return html`
${this.weatherObj.state} ${this._config.name ? html` ${this._config.name} ` : ""} ${this.getUnit("temperature") == "°F" ? Math.round(this.weatherObj.attributes.temperature) : this.weatherObj.attributes.temperature} ${this.getUnit("temperature")}
`; } renderDetails() { const sun = this.hass.states["sun.sun"]; let next_rising; let next_setting; if (sun) { next_rising = new Date(sun.attributes.next_rising); next_setting = new Date(sun.attributes.next_setting); } this.numberElements++; return html` `; } renderForecast() { if (!this.forecast || this.forecast.length === 0) { return html``; } this.numberElements++; if (this._config.graph === true) return this.renderForecastGraph(); else return this.renderForecastTable(); } renderForecastTable() { return html`
${this.forecast.map( forecast => html`
${this.getDateString(forecast.datetime)}
${forecast.temperature}${this.getUnit("temperature")}
${forecast.templow !== undefined ? html`
${forecast.templow}${this.getUnit("temperature")}
` : ""} ${!this._config.hide_precipitation && forecast.precipitation !== undefined && forecast.precipitation !== null ? html`
${forecast.precipitation} ${this.getUnit("precipitation")}
` : ""}
` )}
`; } renderForecastGraph() { this.drawChart(); return html`
${this.forecast .map(forecast => html` `)}
`; } drawChart() { if (!this.forecast) return; var that = this; var dateTime = []; var tempHigh = []; var tempLow = []; var precip = []; for (var i = 0; i < this.forecast.length; i++) { var d = this.forecast[i]; dateTime.push(new Date(d.datetime)); tempHigh.push(d.temperature); tempLow.push(d.templow); precip.push(d.precipitation); } var style = getComputedStyle(document.body); var textColor = style.getPropertyValue('--primary-text-color'); var dividerColor = style.getPropertyValue('--divider-color'); const chartOptions = { type: 'bar', data: { labels: dateTime, datasets: [ { label: this.mode == 'hourly' ? this.ll('temp') : this.ll('tempHi'), type: 'line', data: tempHigh, yAxisID: 'TempAxis', borderWidth: 2.0, lineTension: 0.4, pointRadius: 0.0, pointHitRadius: 5.0, fill: false, }, { label: this.ll('tempLo'), type: 'line', data: tempLow, yAxisID: 'TempAxis', borderWidth: 2.0, lineTension: 0.4, pointRadius: 0.0, pointHitRadius: 5.0, fill: false, }, { label: this.ll('precip'), type: 'bar', data: precip, yAxisID: 'PrecipAxis', }, ] }, options: { animation: { duration: 300, easing: 'linear', onComplete: function () { var chartInstance = this.chart, ctx = chartInstance.ctx; ctx.fillStyle = textColor; var fontSize = 10; var fontStyle = 'normal'; var fontFamily = 'Roboto'; ctx.font = Chart.helpers.fontString(fontSize, fontStyle, fontFamily); ctx.textAlign = 'center'; ctx.textBaseline = 'bottom'; var meta = chartInstance.controller.getDatasetMeta(2); meta.data.forEach(function (bar, index) { var data = (Math.round((chartInstance.data.datasets[2].data[index]) * 10) / 10).toFixed(1); ctx.fillText(data, bar._model.x, bar._model.y - 5); }); }, }, legend: { display: false, }, scales: { xAxes: [{ type: 'time', maxBarThickness: 15, display: false, ticks: { display: false, }, gridLines: { display: false, }, }, { id: 'DateAxis', position: 'top', gridLines: { display: true, drawBorder: false, color: dividerColor, }, ticks: { display: true, source: 'labels', autoSkip: true, fontColor: textColor, maxRotation: 0, callback: function(value, index, values) { return that.getDateString.call(that, value); }, }, }], yAxes: [{ id: 'TempAxis', position: 'left', gridLines: { display: true, drawBorder: false, color: dividerColor, borderDash: [1,3], }, ticks: { display: true, fontColor: textColor, }, afterFit: function(scaleInstance) { scaleInstance.width = 28; }, }, { id: 'PrecipAxis', position: 'right', gridLines: { display: false, drawBorder: false, color: dividerColor, }, ticks: { display: false, min: 0, suggestedMax: 20, fontColor: textColor, }, afterFit: function(scaleInstance) { scaleInstance.width = 15; }, }], }, tooltips: { mode: 'index', callbacks: { title: function (items, data) { const item = items[0]; const date = data.labels[item.index]; return new Date(date).toLocaleDateString(locale, { month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', }); }, label: function(tooltipItems, data) { var label = data.datasets[tooltipItems.datasetIndex].label || ''; if (data.datasets[2].label == label) { return label + ': ' + (tooltipItems.yLabel ? (tooltipItems.yLabel + ' ' + that.getUnit("precipitation")) : ('0 ' + that.getUnit("precipitation"))); } return label + ': ' + tooltipItems.yLabel + ' ' + that.getUnit("temperature"); }, }, }, }, }; this.ChartData = chartOptions; } getWeatherIcon(condition, sun) { return `${ this._config.icons ? this._config.icons : "https://cdn.jsdelivr.net/gh/bramkragten/weather-card/dist/icons/" }${ sun && sun == "below_horizon" ? weatherIconsNight[condition] : weatherIconsDay[condition] }.svg`; } getWindDirIcon(degree) { return cardinalDirectionsIcon[parseInt((degree + 22.5) / 45.0)]; } getWindDir(degree) { if (locale[this.lang] === undefined) return locale.en.cardinalDirections[parseInt((degree + 11.25) / 22.5)]; return locale[this.lang]['cardinalDirections'][parseInt((degree + 11.25) / 22.5)]; } getUnit(measure) { const lengthUnit = this.hass.config.unit_system.length; switch (measure) { case "air_pressure": return lengthUnit === "km" ? this.ll('uPress') : "inHg"; case "length": return lengthUnit; case "precipitation": return lengthUnit === "km" ? this.ll('uPrecip') : "in"; default: return this.hass.config.unit_system[measure] || ""; } } getDateString(datetime) { if (this.mode == 'hourly') { return new Date(datetime).toLocaleTimeString(this.lang, { hour: 'numeric' }); } return new Date(datetime).toLocaleDateString(this.lang, { weekday: 'short' }); } _handleClick() { fireEvent(this, "hass-more-info", { entityId: this._config.entity }); } getCardSize() { return this.numberElements || 3; } renderStyle() { return html` `; } ll(str) { if (locale[this.lang] === undefined) return locale.en[str]; return locale[this.lang][str]; } } customElements.define("weather-card", WeatherCard);