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:
G Johansson 2023-07-21 17:30:59 +02:00 committed by GitHub
parent 0eebc9095c
commit ec58862f3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 460 additions and 56 deletions

View File

@ -24,10 +24,19 @@ import {
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit"; import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { supportsFeature } from "../common/entity/supports-feature";
import { formatNumber } from "../common/number/format_number"; import { formatNumber } from "../common/number/format_number";
import "../components/ha-svg-icon"; import "../components/ha-svg-icon";
import type { HomeAssistant } from "../types"; 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 { interface ForecastAttribute {
temperature: number; temperature: number;
datetime: string; datetime: string;
@ -36,7 +45,7 @@ interface ForecastAttribute {
precipitation_probability?: number; precipitation_probability?: number;
humidity?: number; humidity?: number;
condition?: string; condition?: string;
daytime?: boolean; is_daytime?: boolean;
pressure?: number; pressure?: number;
wind_speed?: string; wind_speed?: string;
} }
@ -45,6 +54,7 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase {
attribution?: string; attribution?: string;
humidity?: number; humidity?: number;
forecast?: ForecastAttribute[]; forecast?: ForecastAttribute[];
is_daytime?: boolean;
pressure?: number; pressure?: number;
temperature?: number; temperature?: number;
visibility?: number; visibility?: number;
@ -57,6 +67,11 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase {
wind_speed_unit: string; wind_speed_unit: string;
} }
export interface ForecastEvent {
type: "hourly" | "daily" | "twice_daily";
forecast: [ForecastAttribute] | null;
}
export interface WeatherEntity extends HassEntityBase { export interface WeatherEntity extends HassEntityBase {
attributes: WeatherEntityAttributes; attributes: WeatherEntityAttributes;
} }
@ -225,9 +240,10 @@ export const getWeatherUnit = (
export const getSecondaryWeatherAttribute = ( export const getSecondaryWeatherAttribute = (
hass: HomeAssistant, hass: HomeAssistant,
stateObj: WeatherEntity stateObj: WeatherEntity,
forecast: ForecastAttribute[]
): TemplateResult | undefined => { ): TemplateResult | undefined => {
const extrema = getWeatherExtrema(hass, stateObj); const extrema = getWeatherExtrema(hass, stateObj, forecast);
if (extrema) { if (extrema) {
return extrema; return extrema;
@ -237,11 +253,11 @@ export const getSecondaryWeatherAttribute = (
let attribute: string; let attribute: string;
if ( if (
stateObj.attributes.forecast?.length && forecast?.length &&
stateObj.attributes.forecast[0].precipitation !== undefined && forecast[0].precipitation !== undefined &&
stateObj.attributes.forecast[0].precipitation !== null forecast[0].precipitation !== null
) { ) {
value = stateObj.attributes.forecast[0].precipitation!; value = forecast[0].precipitation!;
attribute = "precipitation"; attribute = "precipitation";
} else if ("humidity" in stateObj.attributes) { } else if ("humidity" in stateObj.attributes) {
value = stateObj.attributes.humidity!; value = stateObj.attributes.humidity!;
@ -265,9 +281,10 @@ export const getSecondaryWeatherAttribute = (
const getWeatherExtrema = ( const getWeatherExtrema = (
hass: HomeAssistant, hass: HomeAssistant,
stateObj: WeatherEntity stateObj: WeatherEntity,
forecast: ForecastAttribute[]
): TemplateResult | undefined => { ): TemplateResult | undefined => {
if (!stateObj.attributes.forecast?.length) { if (!forecast?.length) {
return undefined; return undefined;
} }
@ -275,18 +292,18 @@ const getWeatherExtrema = (
let tempHigh: number | undefined; let tempHigh: number | undefined;
const today = new Date().getDate(); const today = new Date().getDate();
for (const forecast of stateObj.attributes.forecast!) { for (const fc of forecast!) {
if (new Date(forecast.datetime).getDate() !== today) { if (new Date(fc.datetime).getDate() !== today) {
break; break;
} }
if (!tempHigh || forecast.temperature > tempHigh) { if (!tempHigh || fc.temperature > tempHigh) {
tempHigh = forecast.temperature; tempHigh = fc.temperature;
} }
if (!tempLow || (forecast.templow && forecast.templow < tempLow)) { if (!tempLow || (fc.templow && fc.templow < tempLow)) {
tempLow = forecast.templow; tempLow = fc.templow;
} }
if (!forecast.templow && (!tempLow || forecast.temperature < tempLow)) { if (!fc.templow && (!tempLow || fc.temperature < tempLow)) {
tempLow = forecast.temperature; tempLow = fc.temperature;
} }
} }
@ -510,7 +527,7 @@ export const weatherIcon = (state?: string, nightTime?: boolean): string =>
const DAY_IN_MILLISECONDS = 86400000; const DAY_IN_MILLISECONDS = 86400000;
export const isForecastHourly = ( const isForecastHourly = (
forecast?: ForecastAttribute[] forecast?: ForecastAttribute[]
): boolean | undefined => { ): boolean | undefined => {
if (forecast && forecast?.length && forecast?.length > 2) { if (forecast && forecast?.length && forecast?.length > 2) {
@ -538,3 +555,93 @@ export const getWeatherConvertibleUnits = (
hass.callWS({ hass.callWS({
type: "weather/convertible_units", 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;
};

View File

@ -13,15 +13,18 @@ import {
PropertyValues, PropertyValues,
nothing, nothing,
} from "lit"; } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateWeekdayDay } from "../../../common/datetime/format_date"; import { formatDateWeekdayDay } from "../../../common/datetime/format_date";
import { formatTimeWeekday } from "../../../common/datetime/format_time"; import { formatTimeWeekday } from "../../../common/datetime/format_time";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { import {
getDefaultForecastType,
getForecast,
getWeatherUnit, getWeatherUnit,
getWind, getWind,
isForecastHourly, subscribeForecast,
ForecastEvent,
WeatherEntity, WeatherEntity,
weatherIcons, weatherIcons,
} from "../../../data/weather"; } from "../../../data/weather";
@ -33,6 +36,48 @@ class MoreInfoWeather extends LitElement {
@property() public stateObj?: WeatherEntity; @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 { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("stateObj")) { if (changedProps.has("stateObj")) {
return true; return true;
@ -50,12 +95,33 @@ class MoreInfoWeather extends LitElement {
return false; 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() { protected render() {
if (!this.hass || !this.stateObj) { if (!this.hass || !this.stateObj) {
return nothing; 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` return html`
${this._showValue(this.stateObj.attributes.temperature) ${this._showValue(this.stateObj.attributes.temperature)
@ -144,12 +210,12 @@ class MoreInfoWeather extends LitElement {
</div> </div>
` `
: ""} : ""}
${this.stateObj.attributes.forecast ${forecast
? html` ? html`
<div class="section"> <div class="section">
${this.hass.localize("ui.card.weather.forecast")}: ${this.hass.localize("ui.card.weather.forecast")}:
</div> </div>
${this.stateObj.attributes.forecast.map((item) => ${forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature) this._showValue(item.templow) || this._showValue(item.temperature)
? html`<div class="flex"> ? html`<div class="flex">
${item.condition ${item.condition
@ -176,6 +242,9 @@ class MoreInfoWeather extends LitElement {
this.hass.locale, this.hass.locale,
this.hass.config this.hass.config
)} )}
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize("ui.card.weather.night")}
</div> </div>
`} `}
<div class="templow"> <div class="templow">

View File

@ -20,11 +20,13 @@ import "../../../components/ha-svg-icon";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent } from "../../../data/lovelace";
import { import {
getForecast,
getSecondaryWeatherAttribute, getSecondaryWeatherAttribute,
getWeatherStateIcon, getWeatherStateIcon,
getWeatherUnit, getWeatherUnit,
getWind, getWind,
isForecastHourly, subscribeForecast,
ForecastEvent,
weatherAttrIcons, weatherAttrIcons,
WeatherEntity, WeatherEntity,
weatherSVGStyles, weatherSVGStyles,
@ -41,8 +43,6 @@ import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { WeatherForecastCardConfig } from "./types"; import type { WeatherForecastCardConfig } from "./types";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
const DAY_IN_MILLISECONDS = 86400000;
@customElement("hui-weather-forecast-card") @customElement("hui-weather-forecast-card")
class HuiWeatherForecastCard extends LitElement implements LovelaceCard { class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> { public static async getConfigElement(): Promise<LovelaceCardEditor> {
@ -72,13 +72,54 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
@state() private _config?: WeatherForecastCardConfig; @state() private _config?: WeatherForecastCardConfig;
@state() private _forecastEvent?: ForecastEvent;
@state() private _subscribed?: Promise<() => void>;
@property({ type: Boolean, reflect: true, attribute: "veryverynarrow" }) @property({ type: Boolean, reflect: true, attribute: "veryverynarrow" })
private _veryVeryNarrow = false; private _veryVeryNarrow = false;
private _resizeObserver?: ResizeObserver; 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 { public connectedCallback(): void {
super.connectedCallback(); super.connectedCallback();
if (this.hasUpdated && this._config && this.hass) {
this._subscribeForecastEvents();
}
this.updateComplete.then(() => this._attachObserver()); this.updateComplete.then(() => this._attachObserver());
} }
@ -86,6 +127,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
if (this._resizeObserver) { if (this._resizeObserver) {
this._resizeObserver.disconnect(); this._resizeObserver.disconnect();
} }
this._unsubscribeForecastEvents();
} }
public getCardSize(): number { public getCardSize(): number {
@ -111,7 +153,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
} }
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
return hasConfigOrEntityChanged(this, changedProps); return (
hasConfigOrEntityChanged(this, changedProps) ||
changedProps.has("forecastEvent")
);
} }
public willUpdate(): void { public willUpdate(): void {
@ -130,6 +175,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return; return;
} }
if (changedProps.has("_config") || !this._subscribed) {
this._subscribeForecastEvents();
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined; const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as const oldConfig = changedProps.get("_config") as
| WeatherForecastCardConfig | WeatherForecastCardConfig
@ -172,23 +221,19 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
`; `;
} }
const forecastData = getForecast(
stateObj.attributes,
this._forecastEvent,
this._config?.forecast_type
);
const forecast = const forecast =
this._config?.show_forecast !== false && this._config?.show_forecast !== false && forecastData?.forecast?.length
stateObj.attributes.forecast?.length ? forecastData.forecast.slice(0, this._veryVeryNarrow ? 3 : 5)
? stateObj.attributes.forecast.slice(0, this._veryVeryNarrow ? 3 : 5)
: undefined; : undefined;
const weather = !forecast || this._config?.show_current !== false; const weather = !forecast || this._config?.show_current !== false;
const hourly = isForecastHourly(forecast); const hourly = forecastData?.type === "hourly";
let dayNight: boolean | undefined; const dayNight = forecastData?.type === "twice_daily";
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 weatherStateIcon = getWeatherStateIcon(stateObj.state, this); const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
const name = this._config.name ?? computeStateName(stateObj); 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> </div>
</div> </div>
@ -308,7 +357,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
{ weekday: "short" } { weekday: "short" }
)} )}
<div class="daynight"> <div class="daynight">
${item.daytime === undefined || item.daytime ${item.is_daytime !== false
? this.hass!.localize( ? this.hass!.localize(
"ui.card.weather.day" "ui.card.weather.day"
) )
@ -340,7 +389,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
item.condition!, item.condition!,
this, this,
!( !(
item.daytime || item.daytime === undefined item.is_daytime ||
item.is_daytime === undefined
) )
)} )}
</div> </div>

View File

@ -12,6 +12,7 @@ import {
import { LovelaceHeaderFooterConfig } from "../header-footer/types"; import { LovelaceHeaderFooterConfig } from "../header-footer/types";
import { HaDurationData } from "../../../components/ha-duration-input"; import { HaDurationData } from "../../../components/ha-duration-input";
import { LovelaceTileFeatureConfig } from "../tile-features/types"; import { LovelaceTileFeatureConfig } from "../tile-features/types";
import { ForecastType } from "../../../data/weather";
export interface AlarmPanelCardConfig extends LovelaceCardConfig { export interface AlarmPanelCardConfig extends LovelaceCardConfig {
entity: string; entity: string;
@ -444,6 +445,7 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig {
name?: string; name?: string;
show_current?: boolean; show_current?: boolean;
show_forecast?: boolean; show_forecast?: boolean;
forecast_type?: ForecastType;
secondary_info_attribute?: keyof TranslationDict["ui"]["card"]["weather"]["attributes"]; secondary_info_attribute?: keyof TranslationDict["ui"]["card"]["weather"]["attributes"];
theme?: string; theme?: string;
tap_action?: ActionConfig; tap_action?: ActionConfig;

View File

@ -7,12 +7,14 @@ import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { SchemaUnion } from "../../../../components/ha-form/types";
import { UNAVAILABLE } from "../../../../data/entity"; 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 { HomeAssistant } from "../../../../types";
import type { WeatherForecastCardConfig } from "../../cards/types"; import type { WeatherForecastCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { supportsFeature } from "../../../../common/entity/supports-feature";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
@ -22,6 +24,7 @@ const cardConfigStruct = assign(
theme: optional(string()), theme: optional(string()),
show_current: optional(boolean()), show_current: optional(boolean()),
show_forecast: optional(boolean()), show_forecast: optional(boolean()),
forecast_type: optional(string()),
secondary_info_attribute: optional(string()), secondary_info_attribute: optional(string()),
tap_action: optional(actionConfigStruct), tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct), hold_action: optional(actionConfigStruct),
@ -44,7 +47,7 @@ export class HuiWeatherForecastCardEditor
if ( if (
/* cannot show forecast in case it is unavailable on the entity */ /* 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 */ /* cannot hide both weather and forecast, need one of them */
(config.show_current === false && config.show_forecast === false) (config.show_current === false && config.show_forecast === false)
) { ) {
@ -53,20 +56,72 @@ export class HuiWeatherForecastCardEditor
config: { ...config, show_current: true, show_forecast: false }, 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) { if (this.hass && this._config) {
const stateObj = this.hass.states[this._config.entity] as WeatherEntity; return this.hass.states[this._config.entity] as WeatherEntity;
if (stateObj && stateObj.state !== UNAVAILABLE) {
return !!stateObj.attributes.forecast?.length;
}
} }
return undefined; 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( private _schema = memoizeOne(
(localize: LocalizeFunc, hasForecast?: boolean) => (
localize: LocalizeFunc,
hasForecastLegacy?: boolean,
hasForecastDaily?: boolean,
hasForecastHourly?: boolean,
hasForecastTwiceDaily?: boolean
) =>
[ [
{ {
name: "entity", name: "entity",
@ -86,7 +141,54 @@ export class HuiWeatherForecastCardEditor
{ name: "theme", selector: { theme: {} } }, { 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", name: "forecast",
@ -125,11 +227,17 @@ export class HuiWeatherForecastCardEditor
return nothing; 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 = { const data: WeatherForecastCardConfig = {
show_current: true, show_current: true,
show_forecast: this._has_forecast, show_forecast: this._hasForecast,
...this._config, ...this._config,
}; };
@ -184,6 +292,10 @@ export class HuiWeatherForecastCardEditor
)} (${this.hass!.localize( )} (${this.hass!.localize(
"ui.panel.lovelace.editor.card.config.optional" "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": case "forecast":
return this.hass!.localize( return this.hass!.localize(
"ui.panel.lovelace.editor.card.weather-forecast.weather_to_show" "ui.panel.lovelace.editor.card.weather-forecast.weather_to_show"

View File

@ -16,9 +16,13 @@ import "../../../components/entity/state-badge";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace"; import { ActionHandlerEvent } from "../../../data/lovelace";
import { import {
getDefaultForecastType,
getForecast,
getSecondaryWeatherAttribute, getSecondaryWeatherAttribute,
getWeatherStateIcon, getWeatherStateIcon,
getWeatherUnit, getWeatherUnit,
subscribeForecast,
ForecastEvent,
WeatherEntity, WeatherEntity,
weatherSVGStyles, weatherSVGStyles,
} from "../../../data/weather"; } from "../../../data/weather";
@ -38,6 +42,48 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
@state() private _config?: EntitiesCardEntityConfig; @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 { public setConfig(config: EntitiesCardEntityConfig): void {
if (!config?.entity) { if (!config?.entity) {
throw new Error("Entity must be specified"); throw new Error("Entity must be specified");
@ -50,6 +96,13 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
return hasConfigOrEntityChanged(this, changedProps); return hasConfigOrEntityChanged(this, changedProps);
} }
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (changedProps.has("_config") || !this._subscribed) {
this._subscribeForecastEvents();
}
}
protected render() { protected render() {
if (!this.hass || !this._config) { if (!this.hass || !this._config) {
return nothing; return nothing;
@ -72,6 +125,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
const hasSecondary = this._config.secondary_info; const hasSecondary = this._config.secondary_info;
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
const forecastData = getForecast(stateObj.attributes, this._forecastEvent);
const forecast = forecastData?.forecast;
return html` return html`
<div <div
class="icon-image ${classMap({ class="icon-image ${classMap({
@ -160,7 +216,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
`} `}
</div> </div>
<div class="secondary"> <div class="secondary">
${getSecondaryWeatherAttribute(this.hass!, stateObj)} ${getSecondaryWeatherAttribute(this.hass!, stateObj, forecast!)}
</div> </div>
</div> </div>
`; `;

View File

@ -254,6 +254,9 @@
"day": "Day", "day": "Day",
"night": "Night", "night": "Night",
"forecast": "Forecast", "forecast": "Forecast",
"forecast_daily": "Forecast daily",
"forecast_hourly": "Forecast hourly",
"forecast_twice_daily": "Forecast twice daily",
"high": "High", "high": "High",
"low": "Low" "low": "Low"
} }
@ -4975,7 +4978,12 @@
"weather_to_show": "Weather to Show", "weather_to_show": "Weather to Show",
"show_both": "Show current Weather and Forecast", "show_both": "Show current Weather and Forecast",
"show_only_current": "Show only current Weather", "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": { "view": {