mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-22 00:36:34 +00:00
Weather Card: Beautify (#5387)
* Weather card * Updates * Remove Precipitation from forecast * Weather Card :) * Fix no breaking changes * Size styles * Space * Fix some overlap * Unavailable * New unavailable * Changed to check if less than day * Updates * oops * Little clean up * styling * Reviews * Fix merge * Lint * eslint * New images * Update src/panels/lovelace/editor/config-elements/hui-weather-forecast-card-editor.ts Co-Authored-By: Bram Kragten <mail@bramkragten.nl> * Reviews * Reviews * Comments Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
9a00078169
commit
fdf7b516a0
Binary file not shown.
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 648 B |
Binary file not shown.
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 492 B |
@ -1,24 +1,24 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
import { HomeAssistant, WeatherEntity } from "../types";
|
||||
|
||||
export const weatherImages = {
|
||||
"clear-night": "/static/images/weather/night.png",
|
||||
cloudy: "/static/images/weather/cloudy.png",
|
||||
fog: "/static/images/weather/cloudy.png",
|
||||
hail: "/static/images/weather/rainy.png",
|
||||
lightning: "/static/images/weather/lightning.png",
|
||||
"lightning-rainy": "/static/images/weather/lightning-rainy.png",
|
||||
partlycloudy: "/static/images/weather/partly-cloudy.png",
|
||||
pouring: "/static/images/weather/pouring.png",
|
||||
rainy: "/static/images/weather/rainy.png",
|
||||
snowy: "/static/images/weather/snowy.png",
|
||||
"snowy-rainy": "/static/images/weather/rainy.png",
|
||||
sunny: "/static/images/weather/sunny.png",
|
||||
windy: "/static/images/weather/windy.png",
|
||||
"windy-variant": "/static/images/weather/windy.png",
|
||||
};
|
||||
|
||||
export const weatherIcons = {
|
||||
exceptional: "hass:alert-circle-outline",
|
||||
fog: "hass:weather-fog",
|
||||
hail: "hass:weather-hail",
|
||||
"snowy-rainy": "hass:weather-snowy-rainy",
|
||||
"windy-variant": "hass:weather-windy-variant",
|
||||
};
|
||||
|
||||
export const cardinalDirections = [
|
||||
@ -78,3 +78,89 @@ export const getWeatherUnit = (
|
||||
return hass.config.unit_system[measure] || "";
|
||||
}
|
||||
};
|
||||
|
||||
export const getSecondaryWeatherAttribute = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
): string | undefined => {
|
||||
const extrema = getWeatherExtrema(hass, stateObj);
|
||||
|
||||
if (extrema) {
|
||||
return extrema;
|
||||
}
|
||||
|
||||
let value: number;
|
||||
let attribute: string;
|
||||
|
||||
if (
|
||||
stateObj.attributes.forecast?.length &&
|
||||
stateObj.attributes.forecast[0].precipitation !== undefined &&
|
||||
stateObj.attributes.forecast[0].precipitation !== null
|
||||
) {
|
||||
value = stateObj.attributes.forecast[0].precipitation!;
|
||||
attribute = "precipitation";
|
||||
} else if ("humidity" in stateObj.attributes) {
|
||||
value = stateObj.attributes.humidity!;
|
||||
attribute = "humidity";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `
|
||||
${hass!.localize(
|
||||
`ui.card.weather.attributes.${attribute}`
|
||||
)} ${value} ${getWeatherUnit(hass!, attribute)}
|
||||
`;
|
||||
};
|
||||
|
||||
const getWeatherExtrema = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
): string | undefined => {
|
||||
if (!stateObj.attributes.forecast?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let tempLow: number | undefined;
|
||||
let tempHigh: number | undefined;
|
||||
const today = new Date().getDate();
|
||||
|
||||
for (const forecast of stateObj.attributes.forecast!) {
|
||||
if (new Date(forecast.datetime).getDate() !== today) {
|
||||
break;
|
||||
}
|
||||
if (!tempHigh || forecast.temperature > tempHigh) {
|
||||
tempHigh = forecast.temperature;
|
||||
}
|
||||
if (!tempLow || (forecast.templow && forecast.templow < tempLow)) {
|
||||
tempLow = forecast.templow;
|
||||
}
|
||||
if (!forecast.templow && (!tempLow || forecast.temperature < tempLow)) {
|
||||
tempLow = forecast.temperature;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempLow && !tempHigh) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const unit = getWeatherUnit(hass!, "temperature");
|
||||
|
||||
return `
|
||||
${
|
||||
tempHigh
|
||||
? `
|
||||
${hass!.localize(`ui.card.weather.high`)} ${tempHigh} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${tempLow && tempHigh ? " / " : ""}
|
||||
${
|
||||
tempLow
|
||||
? `
|
||||
${hass!.localize(`ui.card.weather.low`)} ${tempLow} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
@ -8,58 +8,32 @@ import {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-card";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import "../components/hui-warning";
|
||||
|
||||
import { WeatherForecastCardConfig } from "./types";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { HomeAssistant, WeatherEntity } from "../../../types";
|
||||
import { findEntities } from "../common/find-entites";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-warning";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { WeatherForecastCardConfig } from "./types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
weatherIcons,
|
||||
getSecondaryWeatherAttribute,
|
||||
getWeatherUnit,
|
||||
weatherImages,
|
||||
} from "../../../data/weather";
|
||||
import { stateIcon } from "../../../common/entity/state_icon";
|
||||
|
||||
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",
|
||||
};
|
||||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
@customElement("hui-weather-forecast-card")
|
||||
class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
@ -92,8 +66,18 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@property() private _config?: WeatherForecastCardConfig;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "narrow" })
|
||||
private _narrow = false;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.updateComplete.then(() => this._measureCard());
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
return 4;
|
||||
return this._config?.show_forecast !== false ? 4 : 2;
|
||||
}
|
||||
|
||||
public setConfig(config: WeatherForecastCardConfig): void {
|
||||
@ -112,6 +96,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| WeatherForecastCardConfig
|
||||
@ -125,10 +110,6 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
) {
|
||||
applyThemesOnElement(this, this.hass.themes, this._config.theme);
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
toggleAttribute(this, "rtl", computeRTL(this.hass!));
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -136,7 +117,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
const stateObj = this.hass.states[this._config.entity] as WeatherEntity;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
@ -150,9 +131,33 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const forecast = stateObj.attributes.forecast
|
||||
? stateObj.attributes.forecast.slice(0, 5)
|
||||
: undefined;
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return html`
|
||||
<ha-card class="unavailable" @click=${this._handleAction}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.warning.entity_unavailable",
|
||||
"entity",
|
||||
`${computeStateName(stateObj)} (${this._config.entity})`
|
||||
)}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
const forecast =
|
||||
this._config?.show_forecast !== false &&
|
||||
stateObj.attributes.forecast?.length
|
||||
? stateObj.attributes.forecast.slice(0, this._narrow ? 3 : 5)
|
||||
: undefined;
|
||||
|
||||
let hourly: boolean | undefined;
|
||||
|
||||
if (forecast?.length && forecast?.length > 1) {
|
||||
const date1 = new Date(forecast[0].datetime);
|
||||
const date2 = new Date(forecast[1].datetime);
|
||||
const timeDiff = date2.getTime() - date1.getTime();
|
||||
|
||||
hourly = timeDiff < DAY_IN_MILLISECONDS;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
@ -160,130 +165,107 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
.actionHandler=${actionHandler()}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="header">
|
||||
${this.hass.localize(`state.weather.${stateObj.state}`) ||
|
||||
stateObj.state}
|
||||
<div class="name">
|
||||
${(this._config && this._config.name) || computeStateName(stateObj)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="now">
|
||||
<div class="main">
|
||||
${stateObj.state in weatherIcons
|
||||
? html`
|
||||
<ha-icon icon="${weatherIcons[stateObj.state]}"></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
<div class="temp">
|
||||
${stateObj.attributes.temperature}<span
|
||||
>${this.getUnit("temperature")}</span
|
||||
>
|
||||
<div class="icon-info">
|
||||
${stateObj.state in weatherImages
|
||||
? html`
|
||||
<img
|
||||
class="weather-image"
|
||||
src="${weatherImages[stateObj.state]}"
|
||||
/>
|
||||
`
|
||||
: html`
|
||||
<ha-icon
|
||||
class="weather-icon"
|
||||
.icon=${weatherIcons[stateObj.state] || stateIcon(stateObj)}
|
||||
></ha-icon>
|
||||
`}
|
||||
<div class="info">
|
||||
<div class="name">
|
||||
${this._config.name || computeStateName(stateObj)}
|
||||
</div>
|
||||
<div class="state">
|
||||
${this.hass.localize(`state.weather.${stateObj.state}`) ||
|
||||
stateObj.state}
|
||||
</div>
|
||||
</div>
|
||||
<div class="attributes">
|
||||
${this._showValue(stateObj.attributes.pressure)
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.card.weather.attributes.air_pressure"
|
||||
)}:
|
||||
<span class="measurand">
|
||||
${stateObj.attributes.pressure}
|
||||
${this.getUnit("air_pressure")}
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(stateObj.attributes.humidity)
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.card.weather.attributes.humidity"
|
||||
)}:
|
||||
<span class="measurand"
|
||||
>${stateObj.attributes.humidity} %</span
|
||||
>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(stateObj.attributes.wind_speed)
|
||||
? html`
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
"ui.card.weather.attributes.wind_speed"
|
||||
)}:
|
||||
<span class="measurand">
|
||||
${stateObj.attributes.wind_speed}
|
||||
${this.getUnit("length")}/h
|
||||
</span>
|
||||
${this.getWindBearing(stateObj.attributes.wind_bearing)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
<div class="temp-attribute">
|
||||
<div class="temp">
|
||||
${stateObj.attributes.temperature}<span
|
||||
>${getWeatherUnit(this.hass, "temperature")}</span
|
||||
>
|
||||
</div>
|
||||
<div class="attribute">
|
||||
${getSecondaryWeatherAttribute(this.hass, stateObj)}
|
||||
</div>
|
||||
</div>
|
||||
${forecast
|
||||
? html`
|
||||
<div class="forecast">
|
||||
${forecast.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<div class="weekday">
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}<br />
|
||||
${!this._showValue(item.templow)
|
||||
? html`
|
||||
${new Date(item.datetime).toLocaleTimeString(
|
||||
this.hass!.language,
|
||||
{
|
||||
hour: "numeric",
|
||||
}
|
||||
)}
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${this._showValue(item.condition)
|
||||
? html`
|
||||
<div class="icon">
|
||||
<ha-icon
|
||||
.icon="${weatherIcons[item.condition]}"
|
||||
></ha-icon>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(item.temperature)
|
||||
? html`
|
||||
<div class="temp">
|
||||
${item.temperature}
|
||||
${this.getUnit("temperature")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(item.templow)
|
||||
? html`
|
||||
<div class="templow">
|
||||
${item.templow} ${this.getUnit("temperature")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this._showValue(item.precipitation)
|
||||
? html`
|
||||
<div class="precipitation">
|
||||
${item.precipitation}
|
||||
${this.getUnit("precipitation")}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
${forecast
|
||||
? html`
|
||||
<div class="forecast">
|
||||
${forecast.map(
|
||||
(item) => html`
|
||||
<div>
|
||||
<div>
|
||||
${hourly
|
||||
? html`
|
||||
${new Date(item.datetime).toLocaleTimeString(
|
||||
this.hass!.language,
|
||||
{
|
||||
hour: "numeric",
|
||||
}
|
||||
)}
|
||||
`
|
||||
: html`
|
||||
${new Date(item.datetime).toLocaleDateString(
|
||||
this.hass!.language,
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
`}
|
||||
</div>
|
||||
${item.condition !== undefined && item.condition !== null
|
||||
? html`
|
||||
<div class="forecast-image-icon">
|
||||
${item.condition in weatherImages
|
||||
? html`
|
||||
<img
|
||||
class="forecast-image"
|
||||
src="${weatherImages[item.condition]}"
|
||||
/>
|
||||
`
|
||||
: item.condition in weatherIcons
|
||||
? html`
|
||||
<ha-icon
|
||||
class="forecast-icon"
|
||||
.icon=${weatherIcons[item.condition]}
|
||||
></ha-icon>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.temperature !== undefined &&
|
||||
item.temperature !== null
|
||||
? html`
|
||||
<div class="temp">
|
||||
${item.temperature}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${item.templow !== undefined && item.templow !== null
|
||||
? html`
|
||||
<div class="templow">
|
||||
${item.templow}°
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
@ -292,177 +274,239 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this._attachObserver();
|
||||
}
|
||||
|
||||
private _handleAction(): 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 _attachObserver(): void {
|
||||
if (typeof ResizeObserver !== "function") {
|
||||
import("resize-observer").then((modules) => {
|
||||
modules.install();
|
||||
this._attachObserver();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._resizeObserver = new ResizeObserver(
|
||||
debounce(() => this._measureCard(), 250, false)
|
||||
);
|
||||
|
||||
const card = this.shadowRoot!.querySelector("ha-card");
|
||||
// If we show an error or warning there is no ha-card
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
this._resizeObserver.observe(card);
|
||||
}
|
||||
|
||||
private windBearingToText(degree: string): string {
|
||||
const degreenum = parseInt(degree, 10);
|
||||
if (isFinite(degreenum)) {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
|
||||
private _measureCard() {
|
||||
this._narrow = this.offsetWidth < 375;
|
||||
if (this.offsetWidth < 300) {
|
||||
this.setAttribute("verynarrow", "");
|
||||
} else {
|
||||
this.removeAttribute("verynarrow");
|
||||
}
|
||||
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
|
||||
})`;
|
||||
if (this.offsetWidth < 200) {
|
||||
this.setAttribute("veryverynarrow", "");
|
||||
} else {
|
||||
this.removeAttribute("veryverynarrow");
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
|
||||
private _showValue(item: string): boolean {
|
||||
return typeof item !== "undefined" && item !== null;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
cursor: pointer;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 16px;
|
||||
font-size: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
.icon-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:host([rtl]) .name {
|
||||
margin-left: 0px;
|
||||
.weather-image,
|
||||
.weather-icon {
|
||||
flex: 0 0 66px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.now {
|
||||
.weather-icon {
|
||||
--iron-icon-width: 66px;
|
||||
--iron-icon-height: 66px;
|
||||
}
|
||||
|
||||
.info {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state {
|
||||
font-size: 28px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.temp-attribute {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.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;
|
||||
.temp-attribute .temp {
|
||||
position: relative;
|
||||
font-size: 38px;
|
||||
line-height: 1;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
:host([rtl]) .main .temp {
|
||||
direction: ltr;
|
||||
margin-right: 28px;
|
||||
}
|
||||
|
||||
.main .temp span {
|
||||
font-size: 24px;
|
||||
line-height: 1em;
|
||||
.temp-attribute .temp span {
|
||||
position: absolute;
|
||||
font-size: 24px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.measurand {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:host([rtl]) .measurand {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.forecast {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.forecast div {
|
||||
flex: 0 0 auto;
|
||||
.forecast > div {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.forecast .icon {
|
||||
.forecast .icon,
|
||||
.forecast .temp {
|
||||
margin: 4px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
:host([rtl]) .forecast .temp {
|
||||
direction: ltr;
|
||||
.forecast .temp {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.weekday {
|
||||
font-weight: bold;
|
||||
.forecast-image-icon {
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.attributes,
|
||||
.templow,
|
||||
.precipitation {
|
||||
.forecast-image {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.forecast-icon {
|
||||
--iron-icon-width: 40px;
|
||||
--iron-icon-height: 40px;
|
||||
}
|
||||
|
||||
.attribute {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.attribute,
|
||||
.templow {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
:host([rtl]) .precipitation {
|
||||
direction: ltr;
|
||||
:host([narrow]) .weather-image {
|
||||
flex: 0 0 58px;
|
||||
}
|
||||
|
||||
:host([narrow]) .weather-icon {
|
||||
--iron-icon-width: 58px;
|
||||
--iron-icon-height: 58px;
|
||||
}
|
||||
|
||||
:host([narrow]) .state {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
:host([narrow]) .temp-attribute .temp {
|
||||
font-size: 44px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
:host([narrow]) .temp-attribute .temp span {
|
||||
font-size: 18px;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
:host([narrow]) .attribute {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([narrow]) .forecast {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
:host([veryVeryNarrow]) .content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .icon-info {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
:host([narrow]) .weather-image {
|
||||
flex: 0 0 48px;
|
||||
}
|
||||
|
||||
:host([narrow]) .weather-icon {
|
||||
--iron-icon-width: 48px;
|
||||
--iron-icon-height: 48px;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .info {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .temp-attribute .temp {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
:host([veryNarrow]) .temp-attribute .temp span {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
:host([veryVeryNarrow]) .temp-attribute {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.unavailable {
|
||||
height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -274,4 +274,5 @@ export interface ThermostatCardConfig extends LovelaceCardConfig {
|
||||
export interface WeatherForecastCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
name?: string;
|
||||
show_forecast?: boolean;
|
||||
}
|
||||
|
@ -7,12 +7,14 @@ import {
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../components/ha-switch";
|
||||
|
||||
import { EntitiesEditorEvent, EditorTarget } from "../types";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { WeatherForecastCardConfig } from "../../cards/types";
|
||||
import { struct } from "../../common/structs/struct";
|
||||
import "../../components/hui-theme-select-editor";
|
||||
import { LovelaceCardEditor } from "../../types";
|
||||
import { EditorTarget, EntitiesEditorEvent } from "../types";
|
||||
import { configElementStyle } from "./config-elements-style";
|
||||
|
||||
const cardConfigStruct = struct({
|
||||
@ -20,6 +22,7 @@ const cardConfigStruct = struct({
|
||||
entity: "string?",
|
||||
name: "string?",
|
||||
theme: "string?",
|
||||
show_forecast: "boolean?",
|
||||
});
|
||||
|
||||
@customElement("hui-weather-forecast-card-editor")
|
||||
@ -46,6 +49,10 @@ export class HuiWeatherForecastCardEditor extends LitElement
|
||||
return this._config!.theme || "";
|
||||
}
|
||||
|
||||
get _show_forecast(): boolean {
|
||||
return this._config!.show_forecast || true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
@ -61,28 +68,36 @@ export class HuiWeatherForecastCardEditor extends LitElement
|
||||
"ui.panel.lovelace.editor.card.config.required"
|
||||
)})"
|
||||
.hass=${this.hass}
|
||||
.value="${this._entity}"
|
||||
.value=${this._entity}
|
||||
.configValue=${"entity"}
|
||||
.include-domains=${["weather"]}
|
||||
@change="${this._valueChanged}"
|
||||
.includeDomains=${["weather"]}
|
||||
@change=${this._valueChanged}
|
||||
allow-custom-entity
|
||||
></ha-entity-picker>
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.name"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value="${this._name}"
|
||||
.configValue="${"name"}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></paper-input>
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value="${this._theme}"
|
||||
.configValue="${"theme"}"
|
||||
@value-changed="${this._valueChanged}"
|
||||
></hui-theme-select-editor>
|
||||
<div class="side-by-side">
|
||||
<paper-input
|
||||
.label="${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.name"
|
||||
)} (${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})"
|
||||
.value=${this._name}
|
||||
.configValue=${"name"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></paper-input>
|
||||
<hui-theme-select-editor
|
||||
.hass=${this.hass}
|
||||
.value=${this._theme}
|
||||
.configValue=${"theme"}
|
||||
@value-changed=${this._valueChanged}
|
||||
></hui-theme-select-editor>
|
||||
</div>
|
||||
<ha-switch
|
||||
.checked=${this._config!.show_forecast !== false}
|
||||
.configValue=${"show_forecast"}
|
||||
@change=${this._valueChanged}
|
||||
>Show forecast</ha-switch
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -101,7 +116,8 @@ export class HuiWeatherForecastCardEditor extends LitElement
|
||||
} else {
|
||||
this._config = {
|
||||
...this._config,
|
||||
[target.configValue!]: target.value,
|
||||
[target.configValue!]:
|
||||
target.checked !== undefined ? target.checked : target.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,11 @@ import {
|
||||
getWeatherUnit,
|
||||
weatherIcons,
|
||||
weatherImages,
|
||||
getSecondaryWeatherAttribute,
|
||||
} from "../../../data/weather";
|
||||
import { HomeAssistant, WeatherEntity } from "../../../types";
|
||||
import { EntitiesCardEntityConfig } from "../cards/types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-warning";
|
||||
import { LovelaceRow } from "./types";
|
||||
|
||||
@customElement("hui-weather-entity-row")
|
||||
@ -77,95 +77,13 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
`}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${!UNAVAILABLE_STATES.includes(stateObj.state)
|
||||
? this._getSecondaryAttribute(stateObj)
|
||||
: ""}
|
||||
${getSecondaryWeatherAttribute(this.hass!, stateObj)}
|
||||
</div>
|
||||
</div>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getSecondaryAttribute(stateObj: WeatherEntity): string | undefined {
|
||||
const extrema = this._getExtrema(stateObj);
|
||||
|
||||
if (extrema) {
|
||||
return extrema;
|
||||
}
|
||||
|
||||
let value: number;
|
||||
let attribute: string;
|
||||
|
||||
if (
|
||||
stateObj.attributes.forecast?.length &&
|
||||
stateObj.attributes.forecast[0].precipitation !== undefined &&
|
||||
stateObj.attributes.forecast[0].precipitation !== null
|
||||
) {
|
||||
value = stateObj.attributes.forecast[0].precipitation!;
|
||||
attribute = "precipitation";
|
||||
} else if ("humidity" in stateObj.attributes) {
|
||||
value = stateObj.attributes.humidity!;
|
||||
attribute = "humidity";
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `
|
||||
${this.hass!.localize(
|
||||
`ui.card.weather.attributes.${attribute}`
|
||||
)} ${value} ${getWeatherUnit(this.hass!, attribute)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _getExtrema(stateObj: WeatherEntity): string | undefined {
|
||||
if (!stateObj.attributes.forecast?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let tempLow: number | undefined;
|
||||
let tempHigh: number | undefined;
|
||||
const today = new Date().getDate();
|
||||
|
||||
for (const forecast of stateObj.attributes.forecast!) {
|
||||
if (new Date(forecast.datetime).getDate() !== today) {
|
||||
break;
|
||||
}
|
||||
if (!tempHigh || forecast.temperature > tempHigh) {
|
||||
tempHigh = forecast.temperature;
|
||||
}
|
||||
if (!tempLow || (forecast.templow && forecast.templow < tempLow)) {
|
||||
tempLow = forecast.templow;
|
||||
}
|
||||
if (!forecast.templow && (!tempLow || forecast.temperature < tempLow)) {
|
||||
tempLow = forecast.temperature;
|
||||
}
|
||||
}
|
||||
|
||||
if (!tempLow && !tempHigh) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const unit = getWeatherUnit(this.hass!, "temperature");
|
||||
|
||||
return `
|
||||
${
|
||||
tempHigh
|
||||
? `
|
||||
${this.hass!.localize(`ui.card.weather.high`)} ${tempHigh} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
${tempLow && tempHigh ? " / " : ""}
|
||||
${
|
||||
tempLow
|
||||
? `
|
||||
${this.hass!.localize(`ui.card.weather.low`)} ${tempLow} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.attributes {
|
||||
|
@ -1907,7 +1907,8 @@
|
||||
"warning": {
|
||||
"attribute_not_found": "Attribute {attribute} not available in: {entity}",
|
||||
"entity_not_found": "Entity not available: {entity}",
|
||||
"entity_non_numeric": "Entity is non-numeric: {entity}"
|
||||
"entity_non_numeric": "Entity is non-numeric: {entity}",
|
||||
"entity_unavailable": "{entity} is currently unavailable"
|
||||
},
|
||||
"changed_toast": {
|
||||
"message": "The Lovelace UI configuration for this dashboard was updated, refresh to see changes?",
|
||||
|
@ -259,6 +259,7 @@ interface ForecastAttribute {
|
||||
templow?: number;
|
||||
precipitation?: number;
|
||||
humidity?: number;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export type WeatherEntity = HassEntityBase & {
|
||||
|
Loading…
x
Reference in New Issue
Block a user