From 8e0c39e451ff8c8341975bae1027037c98cccbc8 Mon Sep 17 00:00:00 2001 From: Ian Richardson Date: Thu, 5 Sep 2019 22:26:35 -0500 Subject: [PATCH] Convert weather-forecast to LitElement (#3623) * Convert weather-forecast to LitElement Part of https://github.com/home-assistant/home-assistant-polymer/issues/2095 Not sure how RTL works and how to apply it. Also, thinking I should update if the forecast changes and not just the state. Input? * Revert "Convert weather-forecast to LitElement" This reverts commit e1893b0a83f5973df7a28e5d30c3d4d0496155a1. * Convert weather-forecast to LitElement Part of https://github.com/home-assistant/home-assistant-polymer/issues/2095 Not sure how RTL works and how to apply it. Also, thinking I should update if the forecast changes and not just the state. Input? * address review comments and add types * address review comments --- .../cards/hui-weather-forecast-card.js | 24 - .../cards/hui-weather-forecast-card.ts | 433 ++++++++++++++++++ 2 files changed, 433 insertions(+), 24 deletions(-) delete mode 100644 src/panels/lovelace/cards/hui-weather-forecast-card.js create mode 100644 src/panels/lovelace/cards/hui-weather-forecast-card.ts diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.js b/src/panels/lovelace/cards/hui-weather-forecast-card.js deleted file mode 100644 index 9c05fdc2c9..0000000000 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.js +++ /dev/null @@ -1,24 +0,0 @@ -import "../../../cards/ha-weather-card"; - -import LegacyWrapperCard from "./hui-legacy-wrapper-card"; - -class HuiWeatherForecastCard extends LegacyWrapperCard { - static async getConfigElement() { - await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor"); - return document.createElement("hui-weather-forecast-card-editor"); - } - - static getStubConfig() { - return {}; - } - - constructor() { - super("ha-weather-card", "weather"); - } - - getCardSize() { - return 4; - } -} - -customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard); diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts new file mode 100644 index 0000000000..a251c92ab8 --- /dev/null +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -0,0 +1,433 @@ +import { + html, + LitElement, + PropertyValues, + TemplateResult, + css, + CSSResult, + property, + customElement, +} from "lit-element"; + +import "../../../components/ha-card"; +import "../components/hui-warning"; + +import isValidEntityId from "../../../common/entity/valid_entity_id"; +import computeStateName from "../../../common/entity/compute_state_name"; + +import { HomeAssistant } from "../../../types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { WeatherForecastCardConfig } from "./types"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { toggleAttribute } from "../../../common/dom/toggle_attribute"; + +const cardinalDirections = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + "N", +]; + +const weatherIcons = { + "clear-night": "hass:weather-night", + cloudy: "hass:weather-cloudy", + exceptional: "hass:alert-circle-outline", + fog: "hass:weather-fog", + hail: "hass:weather-hail", + lightning: "hass:weather-lightning", + "lightning-rainy": "hass:weather-lightning-rainy", + partlycloudy: "hass:weather-partly-cloudy", + pouring: "hass:weather-pouring", + rainy: "hass:weather-rainy", + snowy: "hass:weather-snowy", + "snowy-rainy": "hass:weather-snowy-rainy", + sunny: "hass:weather-sunny", + windy: "hass:weather-windy", + "windy-variant": "hass:weather-windy-variant", +}; + +@customElement("hui-weather-forecast-card") +class HuiWeatherForecastCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor"); + return document.createElement("hui-weather-forecast-card-editor"); + } + public static getStubConfig(): object { + return { entity: "" }; + } + + @property() public hass?: HomeAssistant; + + @property() private _config?: WeatherForecastCardConfig; + + public getCardSize(): number { + return 4; + } + + public setConfig(config: WeatherForecastCardConfig): void { + if (!config || !config.entity) { + throw new Error("Invalid card configuration"); + } + if (!isValidEntityId(config.entity)) { + throw new Error("Invalid Entity"); + } + + this._config = config; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("hass")) { + toggleAttribute(this, "rtl", computeRTL(this.hass!)); + } + } + + protected render(): TemplateResult | void { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + const forecast = stateObj.attributes.forecast + ? stateObj.attributes.forecast.slice(0, 5) + : undefined; + + return html` + +
+ ${this.hass.localize(`state.weather.${stateObj.state}`) || + stateObj.state} +
+ ${(this._config && this._config.name) || computeStateName(stateObj)} +
+
+
+
+
+ ${stateObj.state in weatherIcons + ? html` + + ` + : ""} +
+ ${stateObj.attributes.temperature}${this.getUnit("temperature")} +
+
+
+ ${this._showValue(stateObj.attributes.pressure) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.air_pressure" + )}: + + ${stateObj.attributes.pressure} + ${this.getUnit("air_pressure")} + +
+ ` + : ""} + ${this._showValue(stateObj.attributes.humidity) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.humidity" + )}: + ${stateObj.attributes.humidity} % +
+ ` + : ""} + ${this._showValue(stateObj.attributes.wind_speed) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.wind_speed" + )}: + + ${stateObj.attributes.wind_speed} + ${this.getUnit("length")}/h + + ${this.getWindBearing(stateObj.attributes.wind_bearing)} +
+ ` + : ""} +
+
+ ${forecast + ? html` +
+ ${forecast.map( + (item) => html` +
+
+ ${new Date(item.datetime).toLocaleDateString( + this.hass!.language, + { weekday: "short" } + )}
+ ${!this._showValue(item.templow) + ? html` + ${new Date(item.datetime).toLocaleTimeString( + this.hass!.language, + { hour: "numeric" } + )} + ` + : ""} +
+ ${this._showValue(item.condition) + ? html` +
+ +
+ ` + : ""} + ${this._showValue(item.temperature) + ? html` +
+ ${item.temperature} + ${this.getUnit("temperature")} +
+ ` + : ""} + ${this._showValue(item.templow) + ? html` +
+ ${item.templow} ${this.getUnit("temperature")} +
+ ` + : ""} + ${this._showValue(item.precipitation) + ? html` +
+ ${item.precipitation} + ${this.getUnit("precipitation")} +
+ ` + : ""} +
+ ` + )} +
+ ` + : ""} +
+
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + private handleClick(): void { + fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); + } + + private getUnit(measure: string): string { + const lengthUnit = this.hass!.config.unit_system.length || ""; + switch (measure) { + case "air_pressure": + return lengthUnit === "km" ? "hPa" : "inHg"; + case "length": + return lengthUnit; + case "precipitation": + return lengthUnit === "km" ? "mm" : "in"; + default: + return this.hass!.config.unit_system[measure] || ""; + } + } + + private windBearingToText(degree: string): string { + const degreenum = parseInt(degree, 10); + if (isFinite(degreenum)) { + // tslint:disable-next-line: no-bitwise + return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16]; + } + return degree; + } + + private getWindBearing(bearing: string): string { + if (bearing != null) { + const cardinalDirection = this.windBearingToText(bearing); + return `(${this.hass!.localize( + `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}` + ) || cardinalDirection})`; + } + return ``; + } + + private _showValue(item: string): boolean { + return typeof item !== "undefined" && item !== null; + } + + static get styles(): CSSResult { + return css` + :host { + cursor: pointer; + } + + .content { + padding: 0 20px 20px; + } + + ha-icon { + color: var(--paper-item-icon-color); + } + + .header { + font-family: var(--paper-font-headline_-_font-family); + -webkit-font-smoothing: var( + --paper-font-headline_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); + text-rendering: var( + --paper-font-common-expensive-kerning_-_text-rendering + ); + opacity: var(--dark-primary-opacity); + padding: 24px 16px 16px; + display: flex; + align-items: baseline; + } + + .name { + margin-left: 16px; + font-size: 16px; + color: var(--secondary-text-color); + } + + :host([rtl]) .name { + margin-left: 0px; + margin-right: 16px; + } + + .now { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + } + + .main { + display: flex; + align-items: center; + margin-right: 32px; + } + + :host([rtl]) .main { + margin-right: 0px; + } + + .main ha-icon { + --iron-icon-height: 72px; + --iron-icon-width: 72px; + margin-right: 8px; + } + + :host([rtl]) .main ha-icon { + margin-right: 0px; + } + + .main .temp { + font-size: 52px; + line-height: 1em; + position: relative; + } + + :host([rtl]) .main .temp { + direction: ltr; + margin-right: 28px; + } + + .main .temp span { + font-size: 24px; + line-height: 1em; + position: absolute; + top: 4px; + } + + .measurand { + display: inline-block; + } + + :host([rtl]) .measurand { + direction: ltr; + } + + .forecast { + margin-top: 16px; + display: flex; + justify-content: space-between; + } + + .forecast div { + flex: 0 0 auto; + text-align: center; + } + + .forecast .icon { + margin: 4px 0; + text-align: center; + } + + :host([rtl]) .forecast .temp { + direction: ltr; + } + + .weekday { + font-weight: bold; + } + + .attributes, + .templow, + .precipitation { + color: var(--secondary-text-color); + } + + :host([rtl]) .precipitation { + direction: ltr; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-weather-forecast-card": HuiWeatherForecastCard; + } +}