mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-18 23:06:40 +00:00
Implement forecast types for Weather (#15028)
* Implement forecast types * editor * Fix twice_daily * All cards * Review comments * hasforecast * card-editor * forecast default * Review comments * fix entity row * Remove legacy option * Check if selected forecast is supported when picking entity * Always show weather_to_show selector * comments * Update types.ts * Hourly before twice-daily * Expose forecast via WS instead of as state attributes * Unsubscribe on disconnect * lint * prettier * Fix _forecastSupported * Improve conditions for subscribing to forecast updates * Teach weather entity row and more info to subscribe * Fix subscribing * Deduplicate code in getForecast * Simplify * Tweak subscribe logic * Address review comments --------- Co-authored-by: Erik <erik@montnemery.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
0eebc9095c
commit
ec58862f3e
@ -24,10 +24,19 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { supportsFeature } from "../common/entity/supports-feature";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum WeatherEntityFeature {
|
||||
FORECAST_DAILY = 1,
|
||||
FORECAST_HOURLY = 2,
|
||||
FORECAST_TWICE_DAILY = 4,
|
||||
}
|
||||
|
||||
export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily";
|
||||
|
||||
interface ForecastAttribute {
|
||||
temperature: number;
|
||||
datetime: string;
|
||||
@ -36,7 +45,7 @@ interface ForecastAttribute {
|
||||
precipitation_probability?: number;
|
||||
humidity?: number;
|
||||
condition?: string;
|
||||
daytime?: boolean;
|
||||
is_daytime?: boolean;
|
||||
pressure?: number;
|
||||
wind_speed?: string;
|
||||
}
|
||||
@ -45,6 +54,7 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase {
|
||||
attribution?: string;
|
||||
humidity?: number;
|
||||
forecast?: ForecastAttribute[];
|
||||
is_daytime?: boolean;
|
||||
pressure?: number;
|
||||
temperature?: number;
|
||||
visibility?: number;
|
||||
@ -57,6 +67,11 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase {
|
||||
wind_speed_unit: string;
|
||||
}
|
||||
|
||||
export interface ForecastEvent {
|
||||
type: "hourly" | "daily" | "twice_daily";
|
||||
forecast: [ForecastAttribute] | null;
|
||||
}
|
||||
|
||||
export interface WeatherEntity extends HassEntityBase {
|
||||
attributes: WeatherEntityAttributes;
|
||||
}
|
||||
@ -225,9 +240,10 @@ export const getWeatherUnit = (
|
||||
|
||||
export const getSecondaryWeatherAttribute = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[]
|
||||
): TemplateResult | undefined => {
|
||||
const extrema = getWeatherExtrema(hass, stateObj);
|
||||
const extrema = getWeatherExtrema(hass, stateObj, forecast);
|
||||
|
||||
if (extrema) {
|
||||
return extrema;
|
||||
@ -237,11 +253,11 @@ export const getSecondaryWeatherAttribute = (
|
||||
let attribute: string;
|
||||
|
||||
if (
|
||||
stateObj.attributes.forecast?.length &&
|
||||
stateObj.attributes.forecast[0].precipitation !== undefined &&
|
||||
stateObj.attributes.forecast[0].precipitation !== null
|
||||
forecast?.length &&
|
||||
forecast[0].precipitation !== undefined &&
|
||||
forecast[0].precipitation !== null
|
||||
) {
|
||||
value = stateObj.attributes.forecast[0].precipitation!;
|
||||
value = forecast[0].precipitation!;
|
||||
attribute = "precipitation";
|
||||
} else if ("humidity" in stateObj.attributes) {
|
||||
value = stateObj.attributes.humidity!;
|
||||
@ -265,9 +281,10 @@ export const getSecondaryWeatherAttribute = (
|
||||
|
||||
const getWeatherExtrema = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
stateObj: WeatherEntity,
|
||||
forecast: ForecastAttribute[]
|
||||
): TemplateResult | undefined => {
|
||||
if (!stateObj.attributes.forecast?.length) {
|
||||
if (!forecast?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -275,18 +292,18 @@ const getWeatherExtrema = (
|
||||
let tempHigh: number | undefined;
|
||||
const today = new Date().getDate();
|
||||
|
||||
for (const forecast of stateObj.attributes.forecast!) {
|
||||
if (new Date(forecast.datetime).getDate() !== today) {
|
||||
for (const fc of forecast!) {
|
||||
if (new Date(fc.datetime).getDate() !== today) {
|
||||
break;
|
||||
}
|
||||
if (!tempHigh || forecast.temperature > tempHigh) {
|
||||
tempHigh = forecast.temperature;
|
||||
if (!tempHigh || fc.temperature > tempHigh) {
|
||||
tempHigh = fc.temperature;
|
||||
}
|
||||
if (!tempLow || (forecast.templow && forecast.templow < tempLow)) {
|
||||
tempLow = forecast.templow;
|
||||
if (!tempLow || (fc.templow && fc.templow < tempLow)) {
|
||||
tempLow = fc.templow;
|
||||
}
|
||||
if (!forecast.templow && (!tempLow || forecast.temperature < tempLow)) {
|
||||
tempLow = forecast.temperature;
|
||||
if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
|
||||
tempLow = fc.temperature;
|
||||
}
|
||||
}
|
||||
|
||||
@ -510,7 +527,7 @@ export const weatherIcon = (state?: string, nightTime?: boolean): string =>
|
||||
|
||||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
export const isForecastHourly = (
|
||||
const isForecastHourly = (
|
||||
forecast?: ForecastAttribute[]
|
||||
): boolean | undefined => {
|
||||
if (forecast && forecast?.length && forecast?.length > 2) {
|
||||
@ -538,3 +555,93 @@ export const getWeatherConvertibleUnits = (
|
||||
hass.callWS({
|
||||
type: "weather/convertible_units",
|
||||
});
|
||||
|
||||
const getLegacyForecast = (
|
||||
weather_attributes?: WeatherEntityAttributes | undefined
|
||||
):
|
||||
| {
|
||||
forecast: ForecastAttribute[];
|
||||
type: "daily" | "hourly" | "twice_daily";
|
||||
}
|
||||
| undefined => {
|
||||
if (weather_attributes?.forecast && weather_attributes.forecast.length > 2) {
|
||||
const hourly = isForecastHourly(weather_attributes.forecast);
|
||||
if (hourly === true) {
|
||||
const dateFirst = new Date(weather_attributes.forecast![0].datetime);
|
||||
const datelast = new Date(
|
||||
weather_attributes.forecast![
|
||||
weather_attributes.forecast!.length - 1
|
||||
].datetime
|
||||
);
|
||||
const dayDiff = datelast.getTime() - dateFirst.getTime();
|
||||
const dayNight = dayDiff > DAY_IN_MILLISECONDS;
|
||||
return {
|
||||
forecast: weather_attributes.forecast,
|
||||
type: dayNight ? "twice_daily" : "hourly",
|
||||
};
|
||||
}
|
||||
return { forecast: weather_attributes.forecast, type: "daily" };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getForecast = (
|
||||
weather_attributes?: WeatherEntityAttributes | undefined,
|
||||
forecast_event?: ForecastEvent,
|
||||
forecast_type?: ForecastType | undefined
|
||||
):
|
||||
| {
|
||||
forecast: ForecastAttribute[];
|
||||
type: "daily" | "hourly" | "twice_daily";
|
||||
}
|
||||
| undefined => {
|
||||
if (forecast_type === undefined) {
|
||||
if (
|
||||
forecast_event?.type !== undefined &&
|
||||
forecast_event?.forecast &&
|
||||
forecast_event?.forecast?.length > 2
|
||||
) {
|
||||
return { forecast: forecast_event.forecast, type: forecast_event?.type };
|
||||
}
|
||||
return getLegacyForecast(weather_attributes);
|
||||
}
|
||||
|
||||
if (forecast_type === "legacy") {
|
||||
return getLegacyForecast(weather_attributes);
|
||||
}
|
||||
|
||||
if (
|
||||
forecast_type === forecast_event?.type &&
|
||||
forecast_event?.forecast &&
|
||||
forecast_event?.forecast?.length > 2
|
||||
) {
|
||||
return { forecast: forecast_event.forecast, type: forecast_type };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const subscribeForecast = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
forecast_type: "daily" | "hourly" | "twice_daily",
|
||||
callback: (forecastevent: ForecastEvent) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ForecastEvent>(callback, {
|
||||
type: "weather/subscribe_forecast",
|
||||
forecast_type,
|
||||
entity_id,
|
||||
});
|
||||
|
||||
export const getDefaultForecastType = (stateObj: HassEntityBase) => {
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) {
|
||||
return "daily";
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) {
|
||||
return "hourly";
|
||||
}
|
||||
if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) {
|
||||
return "twice_daily";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@ -13,15 +13,18 @@ import {
|
||||
PropertyValues,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
|
||||
import { formatTimeWeekday } from "../../../common/datetime/format_time";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
getDefaultForecastType,
|
||||
getForecast,
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
subscribeForecast,
|
||||
ForecastEvent,
|
||||
WeatherEntity,
|
||||
weatherIcons,
|
||||
} from "../../../data/weather";
|
||||
@ -33,6 +36,48 @@ class MoreInfoWeather extends LitElement {
|
||||
|
||||
@property() public stateObj?: WeatherEntity;
|
||||
|
||||
@state() private _forecastEvent?: ForecastEvent;
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
private _unsubscribeForecastEvents() {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeForecastEvents() {
|
||||
this._unsubscribeForecastEvents();
|
||||
if (!this.isConnected || !this.hass || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forecastType = getDefaultForecastType(this.stateObj);
|
||||
if (forecastType) {
|
||||
this._subscribed = subscribeForecast(
|
||||
this.hass!,
|
||||
this.stateObj!.entity_id,
|
||||
forecastType,
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeForecastEvents();
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
if (changedProps.has("stateObj")) {
|
||||
return true;
|
||||
@ -50,12 +95,33 @@ class MoreInfoWeather extends LitElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
|
||||
if (changedProps.has("stateObj") || !this._subscribed) {
|
||||
const oldState = changedProps.get("stateObj") as
|
||||
| WeatherEntity
|
||||
| undefined;
|
||||
if (
|
||||
oldState?.entity_id !== this.stateObj?.entity_id ||
|
||||
!this._subscribed
|
||||
) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const hourly = isForecastHourly(this.stateObj.attributes.forecast);
|
||||
const forecastData = getForecast(
|
||||
this.stateObj.attributes,
|
||||
this._forecastEvent
|
||||
);
|
||||
const forecast = forecastData?.forecast;
|
||||
const hourly = forecastData?.type === "hourly";
|
||||
|
||||
return html`
|
||||
${this._showValue(this.stateObj.attributes.temperature)
|
||||
@ -144,12 +210,12 @@ class MoreInfoWeather extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${this.stateObj.attributes.forecast
|
||||
${forecast
|
||||
? html`
|
||||
<div class="section">
|
||||
${this.hass.localize("ui.card.weather.forecast")}:
|
||||
</div>
|
||||
${this.stateObj.attributes.forecast.map((item) =>
|
||||
${forecast.map((item) =>
|
||||
this._showValue(item.templow) || this._showValue(item.temperature)
|
||||
? html`<div class="flex">
|
||||
${item.condition
|
||||
@ -176,6 +242,9 @@ class MoreInfoWeather extends LitElement {
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize("ui.card.weather.day")
|
||||
: this.hass!.localize("ui.card.weather.night")}
|
||||
</div>
|
||||
`}
|
||||
<div class="templow">
|
||||
|
@ -20,11 +20,13 @@ import "../../../components/ha-svg-icon";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import {
|
||||
getForecast,
|
||||
getSecondaryWeatherAttribute,
|
||||
getWeatherStateIcon,
|
||||
getWeatherUnit,
|
||||
getWind,
|
||||
isForecastHourly,
|
||||
subscribeForecast,
|
||||
ForecastEvent,
|
||||
weatherAttrIcons,
|
||||
WeatherEntity,
|
||||
weatherSVGStyles,
|
||||
@ -41,8 +43,6 @@ import type { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import type { WeatherForecastCardConfig } from "./types";
|
||||
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
|
||||
|
||||
const DAY_IN_MILLISECONDS = 86400000;
|
||||
|
||||
@customElement("hui-weather-forecast-card")
|
||||
class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@ -72,13 +72,54 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _config?: WeatherForecastCardConfig;
|
||||
|
||||
@state() private _forecastEvent?: ForecastEvent;
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "veryverynarrow" })
|
||||
private _veryVeryNarrow = false;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
|
||||
private _needForecastSubscription() {
|
||||
return (
|
||||
this._config!.forecast_type && this._config!.forecast_type !== "legacy"
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribeForecastEvents() {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeForecastEvents() {
|
||||
this._unsubscribeForecastEvents();
|
||||
if (
|
||||
!this.isConnected ||
|
||||
!this.hass ||
|
||||
!this._config ||
|
||||
!this._needForecastSubscription()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribed = subscribeForecast(
|
||||
this.hass!,
|
||||
this._config!.entity,
|
||||
this._config!.forecast_type as "daily" | "hourly" | "twice_daily",
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && this._config && this.hass) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
this.updateComplete.then(() => this._attachObserver());
|
||||
}
|
||||
|
||||
@ -86,6 +127,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
if (this._resizeObserver) {
|
||||
this._resizeObserver.disconnect();
|
||||
}
|
||||
this._unsubscribeForecastEvents();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
@ -111,7 +153,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
return (
|
||||
hasConfigOrEntityChanged(this, changedProps) ||
|
||||
changedProps.has("forecastEvent")
|
||||
);
|
||||
}
|
||||
|
||||
public willUpdate(): void {
|
||||
@ -130,6 +175,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
return;
|
||||
}
|
||||
|
||||
if (changedProps.has("_config") || !this._subscribed) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldConfig = changedProps.get("_config") as
|
||||
| WeatherForecastCardConfig
|
||||
@ -172,23 +221,19 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
const forecastData = getForecast(
|
||||
stateObj.attributes,
|
||||
this._forecastEvent,
|
||||
this._config?.forecast_type
|
||||
);
|
||||
const forecast =
|
||||
this._config?.show_forecast !== false &&
|
||||
stateObj.attributes.forecast?.length
|
||||
? stateObj.attributes.forecast.slice(0, this._veryVeryNarrow ? 3 : 5)
|
||||
this._config?.show_forecast !== false && forecastData?.forecast?.length
|
||||
? forecastData.forecast.slice(0, this._veryVeryNarrow ? 3 : 5)
|
||||
: undefined;
|
||||
const weather = !forecast || this._config?.show_current !== false;
|
||||
|
||||
const hourly = isForecastHourly(forecast);
|
||||
let dayNight: boolean | undefined;
|
||||
|
||||
if (hourly) {
|
||||
const dateFirst = new Date(forecast![0].datetime);
|
||||
const datelast = new Date(forecast![forecast!.length - 1].datetime);
|
||||
const dayDiff = datelast.getTime() - dateFirst.getTime();
|
||||
|
||||
dayNight = dayDiff > DAY_IN_MILLISECONDS;
|
||||
}
|
||||
const hourly = forecastData?.type === "hourly";
|
||||
const dayNight = forecastData?.type === "twice_daily";
|
||||
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
const name = this._config.name ?? computeStateName(stateObj);
|
||||
@ -285,7 +330,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
)}
|
||||
`}
|
||||
`
|
||||
: getSecondaryWeatherAttribute(this.hass, stateObj)}
|
||||
: getSecondaryWeatherAttribute(
|
||||
this.hass,
|
||||
stateObj,
|
||||
forecast!
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,7 +357,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
{ weekday: "short" }
|
||||
)}
|
||||
<div class="daynight">
|
||||
${item.daytime === undefined || item.daytime
|
||||
${item.is_daytime !== false
|
||||
? this.hass!.localize(
|
||||
"ui.card.weather.day"
|
||||
)
|
||||
@ -340,7 +389,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
item.condition!,
|
||||
this,
|
||||
!(
|
||||
item.daytime || item.daytime === undefined
|
||||
item.is_daytime ||
|
||||
item.is_daytime === undefined
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
import { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import { HaDurationData } from "../../../components/ha-duration-input";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
import { ForecastType } from "../../../data/weather";
|
||||
|
||||
export interface AlarmPanelCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
@ -444,6 +445,7 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig {
|
||||
name?: string;
|
||||
show_current?: boolean;
|
||||
show_forecast?: boolean;
|
||||
forecast_type?: ForecastType;
|
||||
secondary_info_attribute?: keyof TranslationDict["ui"]["card"]["weather"]["attributes"];
|
||||
theme?: string;
|
||||
tap_action?: ActionConfig;
|
||||
|
@ -7,12 +7,14 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import type { WeatherEntity } from "../../../../data/weather";
|
||||
import type { ForecastType, WeatherEntity } from "../../../../data/weather";
|
||||
import { WeatherEntityFeature } from "../../../../data/weather";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { WeatherForecastCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { actionConfigStruct } from "../structs/action-struct";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@ -22,6 +24,7 @@ const cardConfigStruct = assign(
|
||||
theme: optional(string()),
|
||||
show_current: optional(boolean()),
|
||||
show_forecast: optional(boolean()),
|
||||
forecast_type: optional(string()),
|
||||
secondary_info_attribute: optional(string()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
hold_action: optional(actionConfigStruct),
|
||||
@ -44,7 +47,7 @@ export class HuiWeatherForecastCardEditor
|
||||
|
||||
if (
|
||||
/* cannot show forecast in case it is unavailable on the entity */
|
||||
(config.show_forecast === true && this._has_forecast === false) ||
|
||||
(config.show_forecast === true && this._hasForecast === false) ||
|
||||
/* cannot hide both weather and forecast, need one of them */
|
||||
(config.show_current === false && config.show_forecast === false)
|
||||
) {
|
||||
@ -53,20 +56,72 @@ export class HuiWeatherForecastCardEditor
|
||||
config: { ...config, show_current: true, show_forecast: false },
|
||||
});
|
||||
}
|
||||
if (
|
||||
!config.forecast_type ||
|
||||
!this._forecastSupported(config.forecast_type)
|
||||
) {
|
||||
let forecastType: string | undefined;
|
||||
if (this._forecastSupported("daily")) {
|
||||
forecastType = "daily";
|
||||
} else if (this._forecastSupported("hourly")) {
|
||||
forecastType = "hourly";
|
||||
} else if (this._forecastSupported("twice_daily")) {
|
||||
forecastType = "twice_daily";
|
||||
} else if (this._forecastSupported("legacy")) {
|
||||
forecastType = "legacy";
|
||||
}
|
||||
fireEvent(this, "config-changed", {
|
||||
config: { ...config, forecast_type: forecastType },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get _has_forecast(): boolean | undefined {
|
||||
private get _stateObj(): WeatherEntity | undefined {
|
||||
if (this.hass && this._config) {
|
||||
const stateObj = this.hass.states[this._config.entity] as WeatherEntity;
|
||||
if (stateObj && stateObj.state !== UNAVAILABLE) {
|
||||
return !!stateObj.attributes.forecast?.length;
|
||||
}
|
||||
return this.hass.states[this._config.entity] as WeatherEntity;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private get _hasForecast(): boolean | undefined {
|
||||
const stateObj = this._stateObj as WeatherEntity;
|
||||
if (stateObj && stateObj.state !== UNAVAILABLE) {
|
||||
return !!(
|
||||
stateObj.attributes.forecast?.length ||
|
||||
stateObj.attributes.supported_features
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _forecastSupported(forecastType: ForecastType): boolean {
|
||||
const stateObj = this._stateObj as WeatherEntity;
|
||||
if (forecastType === "legacy") {
|
||||
return !!stateObj.attributes.forecast?.length;
|
||||
}
|
||||
if (forecastType === "daily") {
|
||||
return supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY);
|
||||
}
|
||||
if (forecastType === "hourly") {
|
||||
return supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY);
|
||||
}
|
||||
if (forecastType === "twice_daily") {
|
||||
return supportsFeature(
|
||||
stateObj,
|
||||
WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, hasForecast?: boolean) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
hasForecastLegacy?: boolean,
|
||||
hasForecastDaily?: boolean,
|
||||
hasForecastHourly?: boolean,
|
||||
hasForecastTwiceDaily?: boolean
|
||||
) =>
|
||||
[
|
||||
{
|
||||
name: "entity",
|
||||
@ -86,7 +141,54 @@ export class HuiWeatherForecastCardEditor
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
...(hasForecast
|
||||
...(!hasForecastLegacy &&
|
||||
(hasForecastDaily || hasForecastHourly || hasForecastTwiceDaily)
|
||||
? ([
|
||||
{
|
||||
name: "forecast_type",
|
||||
selector: {
|
||||
select: {
|
||||
options: [
|
||||
...(hasForecastDaily
|
||||
? ([
|
||||
{
|
||||
value: "daily",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.weather-forecast.daily"
|
||||
),
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
...(hasForecastHourly
|
||||
? ([
|
||||
{
|
||||
value: "hourly",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.weather-forecast.hourly"
|
||||
),
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
...(hasForecastTwiceDaily
|
||||
? ([
|
||||
{
|
||||
value: "twice_daily",
|
||||
label: localize(
|
||||
"ui.panel.lovelace.editor.card.weather-forecast.twice_daily"
|
||||
),
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const)
|
||||
: []),
|
||||
...(hasForecastDaily ||
|
||||
hasForecastHourly ||
|
||||
hasForecastTwiceDaily ||
|
||||
hasForecastLegacy
|
||||
? ([
|
||||
{
|
||||
name: "forecast",
|
||||
@ -125,11 +227,17 @@ export class HuiWeatherForecastCardEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize, this._has_forecast);
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this._forecastSupported("legacy"),
|
||||
this._forecastSupported("daily"),
|
||||
this._forecastSupported("hourly"),
|
||||
this._forecastSupported("twice_daily")
|
||||
);
|
||||
|
||||
const data: WeatherForecastCardConfig = {
|
||||
show_current: true,
|
||||
show_forecast: this._has_forecast,
|
||||
show_forecast: this._hasForecast,
|
||||
...this._config,
|
||||
};
|
||||
|
||||
@ -184,6 +292,10 @@ export class HuiWeatherForecastCardEditor
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
case "forecast_type":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.weather-forecast.forecast_type"
|
||||
);
|
||||
case "forecast":
|
||||
return this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.weather-forecast.weather_to_show"
|
||||
|
@ -16,9 +16,13 @@ import "../../../components/entity/state-badge";
|
||||
import { isUnavailableState } from "../../../data/entity";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace";
|
||||
import {
|
||||
getDefaultForecastType,
|
||||
getForecast,
|
||||
getSecondaryWeatherAttribute,
|
||||
getWeatherStateIcon,
|
||||
getWeatherUnit,
|
||||
subscribeForecast,
|
||||
ForecastEvent,
|
||||
WeatherEntity,
|
||||
weatherSVGStyles,
|
||||
} from "../../../data/weather";
|
||||
@ -38,6 +42,48 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
@state() private _config?: EntitiesCardEntityConfig;
|
||||
|
||||
@state() private _forecastEvent?: ForecastEvent;
|
||||
|
||||
@state() private _subscribed?: Promise<() => void>;
|
||||
|
||||
private _unsubscribeForecastEvents() {
|
||||
if (this._subscribed) {
|
||||
this._subscribed.then((unsub) => unsub());
|
||||
this._subscribed = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeForecastEvents() {
|
||||
this._unsubscribeForecastEvents();
|
||||
if (!this.hass || !this._config || !this.isConnected) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass!.states[this._config!.entity];
|
||||
const forecastType = getDefaultForecastType(stateObj);
|
||||
if (forecastType) {
|
||||
this._subscribed = subscribeForecast(
|
||||
this.hass!,
|
||||
stateObj.entity_id,
|
||||
forecastType,
|
||||
(event) => {
|
||||
this._forecastEvent = event;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeForecastEvents();
|
||||
}
|
||||
|
||||
public setConfig(config: EntitiesCardEntityConfig): void {
|
||||
if (!config?.entity) {
|
||||
throw new Error("Entity must be specified");
|
||||
@ -50,6 +96,13 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("_config") || !this._subscribed) {
|
||||
this._subscribeForecastEvents();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@ -72,6 +125,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
const hasSecondary = this._config.secondary_info;
|
||||
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
|
||||
|
||||
const forecastData = getForecast(stateObj.attributes, this._forecastEvent);
|
||||
const forecast = forecastData?.forecast;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="icon-image ${classMap({
|
||||
@ -160,7 +216,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
`}
|
||||
</div>
|
||||
<div class="secondary">
|
||||
${getSecondaryWeatherAttribute(this.hass!, stateObj)}
|
||||
${getSecondaryWeatherAttribute(this.hass!, stateObj, forecast!)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -254,6 +254,9 @@
|
||||
"day": "Day",
|
||||
"night": "Night",
|
||||
"forecast": "Forecast",
|
||||
"forecast_daily": "Forecast daily",
|
||||
"forecast_hourly": "Forecast hourly",
|
||||
"forecast_twice_daily": "Forecast twice daily",
|
||||
"high": "High",
|
||||
"low": "Low"
|
||||
}
|
||||
@ -4975,7 +4978,12 @@
|
||||
"weather_to_show": "Weather to Show",
|
||||
"show_both": "Show current Weather and Forecast",
|
||||
"show_only_current": "Show only current Weather",
|
||||
"show_only_forecast": "Show only Forecast"
|
||||
"show_only_forecast": "Show only Forecast",
|
||||
"forecast_type": "Select forecast type",
|
||||
"no_type": "No type",
|
||||
"daily": "Daily",
|
||||
"hourly": "Hourly",
|
||||
"twice_daily": "Twice daily"
|
||||
}
|
||||
},
|
||||
"view": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user