Allow customizing weather units (#12947)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: foreign-sub <51928805+foreign-sub@users.noreply.github.com>
This commit is contained in:
Erik Montnemery 2022-06-23 10:48:39 +02:00 committed by GitHub
parent 8bd7370a02
commit 7d118a5715
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 279 additions and 60 deletions

View File

@ -33,6 +33,18 @@ export interface UpdateEntityRegistryEntryResult {
require_restart?: boolean; require_restart?: boolean;
} }
export interface SensorEntityOptions {
unit_of_measurement?: string | null;
}
export interface WeatherEntityOptions {
precipitation_unit?: string | null;
pressure_unit?: string | null;
temperature_unit?: string | null;
visibility_unit?: string | null;
wind_speed_unit?: string | null;
}
export interface EntityRegistryEntryUpdateParams { export interface EntityRegistryEntryUpdateParams {
name?: string | null; name?: string | null;
icon?: string | null; icon?: string | null;
@ -42,9 +54,7 @@ export interface EntityRegistryEntryUpdateParams {
hidden_by: string | null; hidden_by: string | null;
new_entity_id?: string; new_entity_id?: string;
options_domain?: string; options_domain?: string;
options?: { options?: SensorEntityOptions | WeatherEntityOptions;
unit_of_measurement?: string | null;
};
} }
export const findBatteryEntity = ( export const findBatteryEntity = (

View File

@ -37,14 +37,24 @@ interface ForecastAttribute {
humidity?: number; humidity?: number;
condition?: string; condition?: string;
daytime?: boolean; daytime?: boolean;
pressure?: number;
wind_speed?: string;
} }
interface WeatherEntityAttributes extends HassEntityAttributeBase { interface WeatherEntityAttributes extends HassEntityAttributeBase {
temperature: number; attribution?: string;
humidity?: number; humidity?: number;
forecast?: ForecastAttribute[]; forecast?: ForecastAttribute[];
wind_speed: string; pressure?: number;
wind_bearing: string; temperature?: number;
visibility?: number;
wind_bearing?: number | string;
wind_speed?: number;
precipitation_unit: string;
pressure_unit: string;
temperature_unit: string;
visibility_unit: string;
wind_speed_unit: string;
} }
export interface WeatherEntity extends HassEntityBase { export interface WeatherEntity extends HassEntityBase {
@ -138,16 +148,16 @@ const cardinalDirections = [
"N", "N",
]; ];
const getWindBearingText = (degree: string): string => { const getWindBearingText = (degree: number | string): string => {
const degreenum = parseInt(degree, 10); const degreenum = typeof degree === "number" ? degree : parseInt(degree, 10);
if (isFinite(degreenum)) { if (isFinite(degreenum)) {
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16]; return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
} }
return degree; return typeof degree === "number" ? degree.toString() : degree;
}; };
const getWindBearing = (bearing: string): string => { const getWindBearing = (bearing: number | string): string => {
if (bearing != null) { if (bearing != null) {
return getWindBearingText(bearing); return getWindBearingText(bearing);
} }
@ -156,14 +166,19 @@ const getWindBearing = (bearing: string): string => {
export const getWind = ( export const getWind = (
hass: HomeAssistant, hass: HomeAssistant,
speed: string, stateObj: WeatherEntity,
bearing: string speed?: number,
bearing?: number | string
): string => { ): string => {
const speedText = `${formatNumber(speed, hass.locale)} ${getWeatherUnit( const speedText =
hass!, speed !== undefined && speed !== null
"wind_speed" ? `${formatNumber(speed, hass.locale)} ${getWeatherUnit(
)}`; hass!,
if (bearing !== null) { stateObj,
"wind_speed"
)}`
: "-";
if (bearing !== undefined && bearing !== null) {
const cardinalDirection = getWindBearing(bearing); const cardinalDirection = getWindBearing(bearing);
return `${speedText} (${ return `${speedText} (${
hass.localize( hass.localize(
@ -176,19 +191,28 @@ export const getWind = (
export const getWeatherUnit = ( export const getWeatherUnit = (
hass: HomeAssistant, hass: HomeAssistant,
stateObj: WeatherEntity,
measure: string measure: string
): string => { ): string => {
const lengthUnit = hass.config.unit_system.length || ""; const lengthUnit = hass.config.unit_system.length || "";
switch (measure) { switch (measure) {
case "pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "visibility": case "visibility":
case "length": return stateObj.attributes.visibility_unit || lengthUnit;
return lengthUnit;
case "precipitation": case "precipitation":
return lengthUnit === "km" ? "mm" : "in"; return stateObj.attributes.precipitation_unit || lengthUnit === "km"
? "mm"
: "in";
case "pressure":
return stateObj.attributes.pressure_unit || lengthUnit === "km"
? "hPa"
: "inHg";
case "temperature":
return (
stateObj.attributes.temperature_unit ||
hass.config.unit_system.temperature
);
case "wind_speed":
return stateObj.attributes.wind_speed_unit || `${lengthUnit}/h`;
case "humidity": case "humidity":
case "precipitation_probability": case "precipitation_probability":
return "%"; return "%";
@ -233,7 +257,7 @@ export const getSecondaryWeatherAttribute = (
` `
: hass!.localize(`ui.card.weather.attributes.${attribute}`)} : hass!.localize(`ui.card.weather.attributes.${attribute}`)}
${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })} ${formatNumber(value, hass.locale, { maximumFractionDigits: 1 })}
${getWeatherUnit(hass!, attribute)} ${getWeatherUnit(hass!, stateObj, attribute)}
`; `;
}; };
@ -268,7 +292,7 @@ const getWeatherExtrema = (
return undefined; return undefined;
} }
const unit = getWeatherUnit(hass!, "temperature"); const unit = getWeatherUnit(hass!, stateObj, "temperature");
return html` return html`
${tempHigh ? `${formatNumber(tempHigh, hass.locale)} ${unit}` : ""} ${tempHigh ? `${formatNumber(tempHigh, hass.locale)} ${unit}` : ""}

View File

@ -5,7 +5,6 @@ import {
mdiWaterPercent, mdiWaterPercent,
mdiWeatherWindy, mdiWeatherWindy,
} from "@mdi/js"; } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { import {
css, css,
CSSResultGroup, CSSResultGroup,
@ -23,6 +22,7 @@ import {
getWeatherUnit, getWeatherUnit,
getWind, getWind,
isForecastHourly, isForecastHourly,
WeatherEntity,
weatherIcons, weatherIcons,
} from "../../../data/weather"; } from "../../../data/weather";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -31,7 +31,7 @@ import { HomeAssistant } from "../../../types";
class MoreInfoWeather extends LitElement { class MoreInfoWeather extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public stateObj?: HassEntity; @property() public stateObj?: WeatherEntity;
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("stateObj")) { if (changedProps.has("stateObj")) {
@ -58,19 +58,23 @@ class MoreInfoWeather extends LitElement {
const hourly = isForecastHourly(this.stateObj.attributes.forecast); const hourly = isForecastHourly(this.stateObj.attributes.forecast);
return html` return html`
<div class="flex"> ${this._showValue(this.stateObj.attributes.temperature)
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon> ? html`
<div class="main"> <div class="flex">
${this.hass.localize("ui.card.weather.attributes.temperature")} <ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
</div> <div class="main">
<div> ${this.hass.localize("ui.card.weather.attributes.temperature")}
${formatNumber( </div>
this.stateObj.attributes.temperature, <div>
this.hass.locale ${formatNumber(
)} this.stateObj.attributes.temperature!,
${getWeatherUnit(this.hass, "temperature")} this.hass.locale
</div> )}
</div> ${getWeatherUnit(this.hass, this.stateObj, "temperature")}
</div>
</div>
`
: ""}
${this._showValue(this.stateObj.attributes.pressure) ${this._showValue(this.stateObj.attributes.pressure)
? html` ? html`
<div class="flex"> <div class="flex">
@ -80,10 +84,10 @@ class MoreInfoWeather extends LitElement {
</div> </div>
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.pressure, this.stateObj.attributes.pressure!,
this.hass.locale this.hass.locale
)} )}
${getWeatherUnit(this.hass, "pressure")} ${getWeatherUnit(this.hass, this.stateObj, "pressure")}
</div> </div>
</div> </div>
` `
@ -97,7 +101,7 @@ class MoreInfoWeather extends LitElement {
</div> </div>
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.humidity, this.stateObj.attributes.humidity!,
this.hass.locale this.hass.locale
)} )}
% %
@ -115,7 +119,8 @@ class MoreInfoWeather extends LitElement {
<div> <div>
${getWind( ${getWind(
this.hass, this.hass,
this.stateObj.attributes.wind_speed, this.stateObj,
this.stateObj.attributes.wind_speed!,
this.stateObj.attributes.wind_bearing this.stateObj.attributes.wind_bearing
)} )}
</div> </div>
@ -131,10 +136,10 @@ class MoreInfoWeather extends LitElement {
</div> </div>
<div> <div>
${formatNumber( ${formatNumber(
this.stateObj.attributes.visibility, this.stateObj.attributes.visibility!,
this.hass.locale this.hass.locale
)} )}
${getWeatherUnit(this.hass, "length")} ${getWeatherUnit(this.hass, this.stateObj, "visibility")}
</div> </div>
</div> </div>
` `
@ -173,16 +178,24 @@ class MoreInfoWeather extends LitElement {
`} `}
<div class="templow"> <div class="templow">
${this._showValue(item.templow) ${this._showValue(item.templow)
? `${formatNumber(item.templow, this.hass.locale)} ? `${formatNumber(item.templow!, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")}` ${getWeatherUnit(
this.hass,
this.stateObj!,
"temperature"
)}`
: hourly : hourly
? "" ? ""
: "—"} : "—"}
</div> </div>
<div class="temp"> <div class="temp">
${this._showValue(item.temperature) ${this._showValue(item.temperature)
? `${formatNumber(item.temperature, this.hass.locale)} ? `${formatNumber(item.temperature!, this.hass.locale)}
${getWeatherUnit(this.hass, "temperature")}` ${getWeatherUnit(
this.hass,
this.stateObj!,
"temperature"
)}`
: "—"} : "—"}
</div> </div>
</div>` </div>`
@ -240,7 +253,7 @@ class MoreInfoWeather extends LitElement {
`; `;
} }
private _showValue(item: string): boolean { private _showValue(item: number | string | undefined): boolean {
return typeof item !== "undefined" && item !== null; return typeof item !== "undefined" && item !== null;
} }
} }

View File

@ -110,6 +110,14 @@ const OVERRIDE_SENSOR_UNITS = {
pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"], pressure: ["hPa", "Pa", "kPa", "bar", "cbar", "mbar", "mmHg", "inHg", "psi"],
}; };
const OVERRIDE_WEATHER_UNITS = {
precipitation: ["mm", "in"],
pressure: ["hPa", "mbar", "mmHg", "inHg"],
temperature: ["°C", "°F"],
visibility: ["km", "mi"],
wind_speed: ["km/h", "mph", "m/s"],
};
const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"]; const SWITCH_AS_DOMAINS = ["cover", "fan", "light", "lock", "siren"];
@customElement("entity-registry-settings") @customElement("entity-registry-settings")
@ -140,6 +148,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _unit_of_measurement?: string | null; @state() private _unit_of_measurement?: string | null;
@state() private _precipitation_unit?: string | null;
@state() private _pressure_unit?: string | null;
@state() private _temperature_unit?: string | null;
@state() private _visibility_unit?: string | null;
@state() private _wind_speed_unit?: string | null;
@state() private _error?: string; @state() private _error?: string;
@state() private _submitting?: boolean; @state() private _submitting?: boolean;
@ -223,6 +241,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement; this._unit_of_measurement = stateObj?.attributes?.unit_of_measurement;
} }
if (domain === "weather") {
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
this._precipitation_unit = stateObj?.attributes?.precipitation_unit;
this._pressure_unit = stateObj?.attributes?.pressure_unit;
this._temperature_unit = stateObj?.attributes?.temperature_unit;
this._visibility_unit = stateObj?.attributes?.visibility_unit;
this._wind_speed_unit = stateObj?.attributes?.wind_speed_unit;
}
const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain]; const deviceClasses: string[][] = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) { if (!deviceClasses) {
@ -358,6 +386,90 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</ha-select> </ha-select>
` `
: ""} : ""}
${domain === "weather"
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.precipitation_unit"
)}
.value=${this._precipitation_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._precipitationUnitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_WEATHER_UNITS.precipitation.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.pressure_unit"
)}
.value=${this._pressure_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._pressureUnitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_WEATHER_UNITS.pressure.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.temperature_unit"
)}
.value=${this._temperature_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._temperatureUnitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_WEATHER_UNITS.temperature.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.visibility_unit"
)}
.value=${this._visibility_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._visibilityUnitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_WEATHER_UNITS.visibility.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.wind_speed_unit"
)}
.value=${this._wind_speed_unit}
naturalMenuWidth
fixedMenuPosition
@selected=${this._windSpeedUnitChanged}
@closed=${stopPropagation}
>
${OVERRIDE_WEATHER_UNITS.wind_speed.map(
(unit: string) => html`
<mwc-list-item .value=${unit}>${unit}</mwc-list-item>
`
)}
</ha-select>
`
: ""}
${domain === "switch" ${domain === "switch"
? html`<ha-select ? html`<ha-select
.label=${this.hass.localize( .label=${this.hass.localize(
@ -628,6 +740,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._unit_of_measurement = ev.target.value; this._unit_of_measurement = ev.target.value;
} }
private _precipitationUnitChanged(ev): void {
this._error = undefined;
this._precipitation_unit = ev.target.value;
}
private _pressureUnitChanged(ev): void {
this._error = undefined;
this._pressure_unit = ev.target.value;
}
private _temperatureUnitChanged(ev): void {
this._error = undefined;
this._temperature_unit = ev.target.value;
}
private _visibilityUnitChanged(ev): void {
this._error = undefined;
this._visibility_unit = ev.target.value;
}
private _windSpeedUnitChanged(ev): void {
this._error = undefined;
this._wind_speed_unit = ev.target.value;
}
private _switchAsChanged(ev): void { private _switchAsChanged(ev): void {
if (ev.target.value === "") { if (ev.target.value === "") {
return; return;
@ -730,6 +867,23 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
params.options_domain = "sensor"; params.options_domain = "sensor";
params.options = { unit_of_measurement: this._unit_of_measurement }; params.options = { unit_of_measurement: this._unit_of_measurement };
} }
if (
domain === "weather" &&
(stateObj?.attributes?.precipitation_unit !== this._precipitation_unit ||
stateObj?.attributes?.pressure_unit !== this._pressure_unit ||
stateObj?.attributes?.temperature_unit !== this._temperature_unit ||
stateObj?.attributes?.visbility_unit !== this._visibility_unit ||
stateObj?.attributes?.wind_speed_unit !== this._wind_speed_unit)
) {
params.options_domain = "weather";
params.options = {
precipitation_unit: this._precipitation_unit,
pressure_unit: this._pressure_unit,
temperature_unit: this._temperature_unit,
visibility_unit: this._visibility_unit,
wind_speed_unit: this._wind_speed_unit,
};
}
try { try {
const result = await updateEntityRegistryEntry( const result = await updateEntityRegistryEntry(
this.hass!, this.hass!,

View File

@ -228,12 +228,21 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
</div> </div>
<div class="temp-attribute"> <div class="temp-attribute">
<div class="temp"> <div class="temp">
${formatNumber( ${stateObj.attributes.temperature !== undefined &&
stateObj.attributes.temperature, stateObj.attributes.temperature !== null
this.hass.locale ? html`
)}&nbsp;<span ${formatNumber(
>${getWeatherUnit(this.hass, "temperature")}</span stateObj.attributes.temperature,
> this.hass.locale
)}&nbsp;<span
>${getWeatherUnit(
this.hass,
stateObj,
"temperature"
)}</span
>
`
: html`&nbsp;`}
</div> </div>
<div class="attribute"> <div class="attribute">
${this._config.secondary_info_attribute !== undefined ${this._config.secondary_info_attribute !== undefined
@ -255,6 +264,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
"wind_speed" "wind_speed"
? getWind( ? getWind(
this.hass, this.hass,
stateObj,
stateObj.attributes.wind_speed, stateObj.attributes.wind_speed,
stateObj.attributes.wind_bearing stateObj.attributes.wind_bearing
) )
@ -267,6 +277,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
)} )}
${getWeatherUnit( ${getWeatherUnit(
this.hass, this.hass,
stateObj,
this._config.secondary_info_attribute this._config.secondary_info_attribute
)} )}
`} `}

View File

@ -114,7 +114,9 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
})} })}
> >
<div> <div>
${UNAVAILABLE_STATES.includes(stateObj.state) ${UNAVAILABLE_STATES.includes(stateObj.state) ||
stateObj.attributes.temperature === undefined ||
stateObj.attributes.temperature === null
? computeStateDisplay( ? computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
@ -125,7 +127,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
stateObj.attributes.temperature, stateObj.attributes.temperature,
this.hass.locale this.hass.locale
)} )}
${getWeatherUnit(this.hass, "temperature")} ${getWeatherUnit(this.hass, stateObj, "temperature")}
`} `}
</div> </div>
<div class="secondary"> <div class="secondary">

View File

@ -811,6 +811,11 @@
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'", "icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"entity_id": "Entity ID", "entity_id": "Entity ID",
"unit_of_measurement": "Unit of Measurement", "unit_of_measurement": "Unit of Measurement",
"precipitation_unit": "Precipitation unit",
"pressure_unit": "Barometric pressure unit",
"temperature_unit": "Temperature unit",
"visibility_unit": "Visibility unit",
"wind_speed_unit": "Wind speed unit",
"device_class": "Show as", "device_class": "Show as",
"device_classes": { "device_classes": {
"binary_sensor": { "binary_sensor": {