From abc5128446e58eec8f9298ad9d836c188f84c9e6 Mon Sep 17 00:00:00 2001 From: troinine Date: Thu, 25 Dec 2025 16:14:05 +0200 Subject: [PATCH 01/17] feat: support for displaying current weather attributes --- .../wfc-current-weather-attributes.ts | 111 ++++++++++++++++++ src/components/wfc-current-weather.ts | 109 +++++++++++------ src/data/weather.ts | 70 ++++++++++- src/weather-forecast-card.css | 36 ++++++ 4 files changed, 292 insertions(+), 34 deletions(-) create mode 100644 src/components/wfc-current-weather-attributes.ts diff --git a/src/components/wfc-current-weather-attributes.ts b/src/components/wfc-current-weather-attributes.ts new file mode 100644 index 0000000..a51b2c5 --- /dev/null +++ b/src/components/wfc-current-weather-attributes.ts @@ -0,0 +1,111 @@ +import { html, LitElement, nothing, TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { + CurrentWeatherAttributes, + ExtendedHomeAssistant, + WeatherForecastCardConfig, +} from "../types"; +import { getWind, WeatherEntity } from "../data/weather"; +import { capitalize } from "lodash-es"; +import memoizeOne from "memoize-one"; + +const ATTRIBUTE_ICON_MAP: { [key in CurrentWeatherAttributes]: string } = { + humidity: "mdi:cloud-percent-outline", + pressure: "mdi:gauge", + wind_speed: "mdi:weather-windy-variant", + wind_gust_speed: "mdi:weather-windy", + visibility: "mdi:eye", + ozone: "mdi:molecule", + uv_index: "mdi:weather-sunny-alert", + dew_point: "mdi:water-thermometer-outline", +}; + +@customElement("wfc-current-weather-attributes") +export class WfcCurrentWeatherAttributes extends LitElement { + @property({ attribute: false }) hass!: ExtendedHomeAssistant; + @property({ attribute: false }) weatherEntity!: WeatherEntity; + @property({ attribute: false }) + weatherAttributes: CurrentWeatherAttributes[] = []; + @property({ attribute: false }) config!: WeatherForecastCardConfig; + + protected createRenderRoot() { + return this; + } + + protected render(): TemplateResult | typeof nothing { + if ( + !this.hass || + !this.weatherEntity || + this.weatherAttributes.length === 0 + ) { + return nothing; + } + + return html` +
+ ${this.weatherAttributes.map((attribute) => { + const value = this.computeAttributeValue(attribute); + + if (!value) return nothing; + + return html` +
+ + + ${this.localize(attribute)} + + ${value} +
+ `; + })} +
+ `; + } + + private computeAttributeValue = ( + attribute: CurrentWeatherAttributes + ): string | undefined => { + const stateObj = this.weatherEntity; + + if (stateObj.attributes[attribute] === undefined) { + return undefined; + } + + if (attribute === "wind_speed") { + return getWind(this.hass, stateObj); + } + + return this.hass.formatEntityAttributeValue?.( + this.weatherEntity, + attribute + ); + }; + + private localize = (attribute: string): string => { + return ( + this.hass.localize(getLocalizationKey(attribute)) || + capitalize(attribute).replace(/_/g, " ") + ); + }; +} + +const getLocalizationKey = memoizeOne((attribute: string): string => { + switch (attribute) { + case "pressure": + return "ui.card.weather.attributes.air_pressure"; + default: + return `ui.card.weather.attributes.${attribute}`; + } +}); + +declare global { + interface HTMLElementTagNameMap { + "wfc-current-weather-attributes": WfcCurrentWeatherAttributes; + } +} diff --git a/src/components/wfc-current-weather.ts b/src/components/wfc-current-weather.ts index 634aa3b..6e2c5e1 100644 --- a/src/components/wfc-current-weather.ts +++ b/src/components/wfc-current-weather.ts @@ -10,6 +10,8 @@ import { hasAction, } from "custom-card-helpers"; import { + CURRENT_WEATHER_ATTRIBUTES, + CurrentWeatherAttributes, ExtendedHomeAssistant, TemperatureHighLow, TemperatureInfo, @@ -22,6 +24,7 @@ import { } from "../data/weather"; import "./wfc-weather-condition-icon-provider"; +import "./wfc-current-weather-attributes"; @customElement("wfc-current-weather") export class WfcCurrentWeather extends LitElement { @@ -35,7 +38,7 @@ export class WfcCurrentWeather extends LitElement { } render(): TemplateResult | typeof nothing { - if (!this.weatherEntity) { + if (!this.hass || !this.weatherEntity) { return nothing; } @@ -49,6 +52,7 @@ export class WfcCurrentWeather extends LitElement { this.config.forecast?.show_sun_times && suntimesInfo ? suntimesInfo.isNightTime : false; + const attributes = this.getConiguredAttributes(); return html`
@@ -63,45 +67,84 @@ export class WfcCurrentWeather extends LitElement {
${this.hass?.formatEntityState?.(this.weatherEntity)}
- ${name - ? html`
${name}
` - : nothing} + ${ + name + ? html`
${name}
` + : nothing + }
- ${tempInfo !== null - ? html` -
-
- ${tempInfo.temperature}${tempInfo.temperatureUnit} + ${ + tempInfo !== null + ? html` +
+
+ ${tempInfo.temperature}${tempInfo.temperatureUnit} +
+ ${tempHighLowInfo + ? html` +
+ ${tempHighLowInfo.temperatureHigh}${tempHighLowInfo.temperatureHighLowUnit} + / + ${tempHighLowInfo.temperatureLow}${tempHighLowInfo.temperatureHighLowUnit} +
+ ` + : nothing}
- ${tempHighLowInfo - ? html` -
- ${tempHighLowInfo.temperatureHigh}${tempHighLowInfo.temperatureHighLowUnit} - / - ${tempHighLowInfo.temperatureLow}${tempHighLowInfo.temperatureHighLowUnit} -
- ` - : nothing} -
- ` - : nothing} + ` + : nothing + } +
+ ${ + attributes.length > 0 + ? html`` + : nothing + } `; } + private getConiguredAttributes(): CurrentWeatherAttributes[] { + const showAttr = this.config.current?.show_attributes; + + if (showAttr === undefined || showAttr === null) { + return []; + } + + if (Array.isArray(showAttr)) { + return showAttr; + } + + if (typeof showAttr === "boolean") { + return showAttr ? [...CURRENT_WEATHER_ATTRIBUTES] : []; + } + + if (typeof showAttr === "string") { + return [showAttr as CurrentWeatherAttributes]; + } + + return []; + } + private onAction = (event: ActionHandlerEvent): void => { const config = this.config.temperature_entity ? { diff --git a/src/data/weather.ts b/src/data/weather.ts index 35104e3..7553c5b 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -40,7 +40,7 @@ export interface ForecastAttribute { wind_bearing?: number; } -interface WeatherEntityAttributes extends HassEntityAttributeBase { +export interface WeatherEntityAttributes extends HassEntityAttributeBase { attribution?: string; humidity?: number; forecast?: ForecastAttribute[]; @@ -55,6 +55,10 @@ interface WeatherEntityAttributes extends HassEntityAttributeBase { temperature_unit: string; visibility_unit: string; wind_speed_unit: string; + wind_gust_speed?: number; + dew_point?: number; + uv_index?: number; + ozone?: number; } export interface ForecastEvent { @@ -66,6 +70,70 @@ export interface WeatherEntity extends HassEntityBase { attributes: WeatherEntityAttributes; } +const cardinalDirections = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + "N", +]; + +const getWindBearingText = (degree: number | string): string | undefined => { + const degreenum = typeof degree === "number" ? degree : parseInt(degree, 10); + if (isFinite(degreenum)) { + return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16]; + } + + return typeof degree === "number" ? degree.toString() : degree; +}; + +const getWindBearing = (bearing: number | string): string | undefined => { + if (bearing != null) { + return getWindBearingText(bearing); + } + + return ""; +}; + +export const getWind = ( + hass: ExtendedHomeAssistant, + stateObj: WeatherEntity +): string | undefined => { + const windSpeed = stateObj.attributes.wind_speed; + const windBearing = stateObj.attributes.wind_bearing; + + if (windSpeed === null || windSpeed === undefined) { + return undefined; + } + + const speedText = hass.formatEntityAttributeValue(stateObj, "wind_speed"); + + if (windBearing != null && windBearing !== undefined) { + const cardinalDirection = getWindBearing(windBearing); + + if (cardinalDirection) { + return `${speedText} (${ + hass.localize( + `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}` + ) || cardinalDirection + })`; + } + } + return speedText; +}; + export const getWeatherUnit = ( hass: ExtendedHomeAssistant, stateObj: WeatherEntity, diff --git a/src/weather-forecast-card.css b/src/weather-forecast-card.css index d840f69..d6c3d15 100644 --- a/src/weather-forecast-card.css +++ b/src/weather-forecast-card.css @@ -122,6 +122,42 @@ ha-card { white-space: nowrap; } +.wfc-current-attributes { + display: flex; + flex-direction: column; + gap: var(--ha-space-1, 4px); + margin: var(--ha-space-3, 12px) 0; +} + +.wfc-current-attribute { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: var(--ha-space-1, 4px); + font-size: var(--ha-font-size-m, 14px); + line-height: var(--ha-line-height-condensed); + color: var(--primary-text-color, #212121); + white-space: nowrap; +} + +.wfc-current-attribute-icon { + color: var(--state-icon-color, #616161); + padding-left: var(--ha-space-2, 8px); +} + +.wfc-current-attribute-value, +.wfc-current-attribute-name { + overflow: hidden; + white-space: nowrap; +} + +.wfc-current-attribute-name { + text-overflow: ellipsis; + padding-left: var(--ha-space-4, 16px); + flex: 1; +} + .wfc-forecast-container { --mask: linear-gradient( to right, From f3893a65875098a683871d8881de59e2d5f41eac Mon Sep 17 00:00:00 2001 From: troinine Date: Thu, 25 Dec 2025 16:17:05 +0200 Subject: [PATCH 02/17] feat: support for displaying current weather attributes --- src/types.ts | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index 68365f8..04d8006 100644 --- a/src/types.ts +++ b/src/types.ts @@ -17,6 +17,17 @@ export type ForecastActionEvent = HASSDomEvent; export type ForecastActionHandler = (event: ForecastActionEvent) => void; +export const CURRENT_WEATHER_ATTRIBUTES = [ + "humidity", + "pressure", + "wind_speed", + "wind_gust_speed", + "visibility", + "ozone", + "uv_index", + "dew_point", +] as const; + export const WEATHER_EFFECTS = [ "rain", "snow", @@ -26,6 +37,9 @@ export const WEATHER_EFFECTS = [ "sun", ] as const; +export type CurrentWeatherAttributes = + (typeof CURRENT_WEATHER_ATTRIBUTES)[number]; + export type WeatherEffect = (typeof WEATHER_EFFECTS)[number]; export enum ForecastMode { @@ -45,6 +59,10 @@ export interface WeatherForecastCardForecastConfig { scroll_to_selected?: boolean; } +export interface WeatherForecastCardCurrentConfig { + show_attributes?: CurrentWeatherAttributes | CurrentWeatherAttributes[]; +} + export interface WeatherForecastCardForecastActionConfig { tap_action?: ForecastActionConfig; hold_action?: ForecastActionConfig; @@ -61,6 +79,7 @@ export interface WeatherForecastCardConfig { default_forecast?: "hourly" | "daily"; icons_path?: string; show_condition_effects?: boolean | WeatherEffect[]; + current?: WeatherForecastCardCurrentConfig; forecast?: WeatherForecastCardForecastConfig; forecast_action?: WeatherForecastCardForecastActionConfig; tap_action?: ActionConfig | undefined; @@ -71,11 +90,11 @@ export interface WeatherForecastCardConfig { export type ForecastActionConfig = ForecastToggleActionConfig | ActionConfig; export type ExtendedHomeAssistant = HomeAssistant & { - formatEntityState?: (stateObj: HassEntity) => string | undefined; - formatEntityAttributeValue?: ( + formatEntityState: (stateObj: HassEntity) => string | undefined; + formatEntityAttributeValue: ( stateObj: HassEntity, attribute: string - ) => unknown; + ) => string | undefined; themes?: { darkMode: boolean; }; From eae19f032499e8a2cefd1e7bb5cd49374d348c92 Mon Sep 17 00:00:00 2001 From: troinine Date: Thu, 25 Dec 2025 16:40:59 +0200 Subject: [PATCH 03/17] chore: updated test applications --- demo/index.html | 3 +- package.json | 1 + pnpm-lock.yaml | 8 ++++ test/app/index.html | 20 +++++++++- test/mocks/ha-attribute-icon.ts | 71 +++++++++++++++++++++++++++++++++ test/mocks/hass.ts | 57 +++++++++++++++++++++++++- test/mocks/index.ts | 3 ++ 7 files changed, 158 insertions(+), 5 deletions(-) create mode 100644 test/mocks/ha-attribute-icon.ts create mode 100644 test/mocks/index.ts diff --git a/demo/index.html b/demo/index.html index 3db3e44..f5ec977 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,6 +23,7 @@ font-family: "Roboto", "Noto", sans-serif; color: var(--primary-text-color); --card-padding: 24px; + --state-icon-color: #44739e; } .light-theme { --primary-text-color: #141414; @@ -324,7 +325,7 @@

For Home Assistant