Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,37 @@ resources:
| `default_forecast` | `string` | `daily` | Default forecast to view (`daily` or `hourly`). |
| `icons_path` | `string` | optional | Path to custom icons. For example, `/local/img/my-custom-weather-icons`. See [Custom Weather Icons](#custom-weather-icons) for more details. |
| `show_condition_effects` | `boolean`/`array` | optional | Enable animated weather condition effects. Set to `true` for all conditions or provide an array of specific effects. See [Weather Condition Effects](#weather-condition-effects). |
| `current` | `object` | optional | Current weather configuration options. See [Current Object](#current-object). |
| `forecast` | `object` | optional | Forecast configuration options. See [Forecast Object](#forecast-object). |
| `forecast_action` | `object` | optional | Actions for the forecast area. See [Forecast Actions](#forecast-actions). |
| `tap_action` | `object` | optional | Defines the type of action to perform on tap for the main card. Action defaults to `more-info`. See [Home Assistant Actions](https://www.home-assistant.io/dashboards/actions/). |
| `hold_action` | `object` | optional | Defines the type of action to perform on hold for the main card. See [Home Assistant Actions](https://www.home-assistant.io/dashboards/actions/). |
| `double_tap_action` | `object` | optional | Defines the type of action to perform on double click for the main card. See [Home Assistant Actions](https://www.home-assistant.io/dashboards/actions/). |

### Current Object

The `current` object controls the display of current weather information and attributes.

| Name | Type | Default | Description |
| :---------------- | :------------------------- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `show_attributes` | `boolean`/`string`/`array` | optional | Display weather attributes below current conditions. Set to `true` to show all available attributes, `false` to hide all, a single attribute name (e.g., `"humidity"`), or an array of attribute names (e.g., `["humidity", "pressure"]`). |

**Available attributes:**

- `humidity` - Relative humidity percentage
- `pressure` - Atmospheric pressure
- `wind_speed` - Wind speed with direction (if available)
- `wind_gust_speed` - Wind gust speed
- `visibility` - Visibility distance
- `ozone` - Ozone level
- `uv_index` - UV index
- `dew_point` - Dew point temperature
- `apparent_temperature` - Feels like temperature
- `cloud_coverage` - Cloud coverage percentage

> [!NOTE]
> Attributes are only rendered if the data is available from your weather entity. If an attribute is not provided by your weather integration, it will not be displayed even if configured.

### Forecast Object

| Name | Type | Default | Description |
Expand Down Expand Up @@ -113,6 +138,11 @@ Forecast actions have the following options
```yaml
type: custom:weather-forecast-card
entity: weather.home
current:
show_attributes:
- humidity
- pressure
- wind_speed
forecast:
mode: chart
extra_attribute: wind_direction
Expand Down
3 changes: 2 additions & 1 deletion demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -324,7 +325,7 @@ <h3>For Home Assistant</h3>
</div>
<script type="module" src="../src/index.ts"></script>
<script type="module">
import { MockHass } from "../test/mocks/hass.ts";
import { MockHass } from "../test/mocks/index.ts";

const mockHass = new MockHass();
let theme = "dark";
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@mdi/js": "^7.4.47",
"@open-wc/testing": "^4.0.0",
"@parcel/transformer-inline-string": "^2.16.3",
"@types/lodash-es": "^4.17.12",
Expand Down
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

140 changes: 140 additions & 0 deletions src/components/wfc-current-weather-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { html, LitElement, nothing, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators.js";
import {
CurrentWeatherAttributes,
ExtendedHomeAssistant,
WeatherForecastCardConfig,
} from "../types";
import { getWeatherUnit, 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",
apparent_temperature: "mdi:thermometer",
cloud_coverage: "mdi:cloud-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;
}

const attributeTemplates = this.weatherAttributes
.map((attr) => this._renderAttribute(attr))
.filter((template) => template !== nothing);

if (attributeTemplates.length === 0) {
return nothing;
}

return html`
<div class="wfc-current-attributes">${attributeTemplates}</div>
`;
}

private _renderAttribute(
attribute: CurrentWeatherAttributes
): TemplateResult | typeof nothing {
const value = this.computeAttributeValue(attribute);

if (!value) {
return nothing;
}

return html`
<div class="wfc-current-attribute">
<ha-attribute-icon
class="wfc-current-attribute-icon"
.hass=${this.hass}
.stateObj=${this.weatherEntity}
.attribute=${attribute}
.icon=${ATTRIBUTE_ICON_MAP[attribute]}
></ha-attribute-icon>
<span class="wfc-current-attribute-name">
${this.localize(attribute)}
</span>
<span class="wfc-current-attribute-value">${value}</span>
</div>
`;
}

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);
}

// hass.formatEntityAttributeValue does not support wind_gust_speed yet
if (
attribute === "wind_gust_speed" &&
stateObj.attributes.wind_gust_speed !== undefined
) {
const unit = getWeatherUnit(this.hass, stateObj, "wind_gust_speed");

return `${stateObj.attributes.wind_gust_speed} ${unit}`;
}

// hass.formatEntityAttributeValue does not support ozone yet
if (attribute === "ozone" && stateObj.attributes.ozone !== undefined) {
const unit = getWeatherUnit(this.hass, stateObj, "ozone");

return `${stateObj.attributes.ozone} ${unit}`;
}

return this.hass.formatEntityAttributeValue(this.weatherEntity, attribute);
};

private localize = (attribute: string): string => {
return (
this.hass.formatEntityAttributeName(this.weatherEntity, attribute) ||
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;
}
}
96 changes: 66 additions & 30 deletions src/components/wfc-current-weather.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
hasAction,
} from "custom-card-helpers";
import {
CURRENT_WEATHER_ATTRIBUTES,
CurrentWeatherAttributes,
ExtendedHomeAssistant,
TemperatureHighLow,
TemperatureInfo,
Expand All @@ -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 {
Expand All @@ -35,7 +38,7 @@ export class WfcCurrentWeather extends LitElement {
}

render(): TemplateResult | typeof nothing {
if (!this.weatherEntity) {
if (!this.hass || !this.weatherEntity) {
return nothing;
}

Expand All @@ -49,6 +52,7 @@ export class WfcCurrentWeather extends LitElement {
this.config.forecast?.show_sun_times && suntimesInfo
? suntimesInfo.isNightTime
: false;
const attributes = this.getConfiguredAttributes();

return html`
<div class="wfc-current-weather">
Expand All @@ -67,41 +71,73 @@ export class WfcCurrentWeather extends LitElement {
? html`<div class="wfc-name wfc-secondary">${name}</div>`
: nothing}
</div>
</div>
${tempInfo !== null
? html`
<div class="wfc-current-temperatures">
<div
class="wfc-current-temperature"
.actionHandler=${actionHandler({
stopPropagation: true,
hasHold: hasAction(this.config.hold_action as ActionConfig),
hasDoubleClick: hasAction(
this.config.double_tap_action as ActionConfig
),
})}
@action=${this.onAction}
>
${tempInfo.temperature}${tempInfo.temperatureUnit}
${tempInfo !== null
? html`
<div class="wfc-current-temperatures">
<div
class="wfc-current-temperature"
.actionHandler=${actionHandler({
stopPropagation: true,
hasHold: hasAction(
this.config.hold_action as ActionConfig
),
hasDoubleClick: hasAction(
this.config.double_tap_action as ActionConfig
),
})}
@action=${this.onAction}
>
${tempInfo.temperature}${tempInfo.temperatureUnit}
</div>
${tempHighLowInfo
? html`
<div
class="wfc-current-temperature-high-low wfc-secondary"
>
${tempHighLowInfo.temperatureHigh}${tempHighLowInfo.temperatureHighLowUnit}
/
${tempHighLowInfo.temperatureLow}${tempHighLowInfo.temperatureHighLowUnit}
</div>
`
: nothing}
</div>
${tempHighLowInfo
? html`
<div
class="wfc-current-temperature-high-low wfc-secondary"
>
${tempHighLowInfo.temperatureHigh}${tempHighLowInfo.temperatureHighLowUnit}
/
${tempHighLowInfo.temperatureLow}${tempHighLowInfo.temperatureHighLowUnit}
</div>
`
: nothing}
</div>
`
`
: nothing}
</div>
${attributes.length > 0
? html`<wfc-current-weather-attributes
.hass=${this.hass}
.weatherEntity=${this.weatherEntity}
.config=${this.config}
.weatherAttributes=${attributes}
></wfc-current-weather-attributes>`
: nothing}
</div>
`;
}

private getConfiguredAttributes(): 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
? {
Expand Down
Loading