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;
}
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 {
name?: string | null;
icon?: string | null;
@ -42,9 +54,7 @@ export interface EntityRegistryEntryUpdateParams {
hidden_by: string | null;
new_entity_id?: string;
options_domain?: string;
options?: {
unit_of_measurement?: string | null;
};
options?: SensorEntityOptions | WeatherEntityOptions;
}
export const findBatteryEntity = (

View File

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

View File

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

View File

@ -110,6 +110,14 @@ const OVERRIDE_SENSOR_UNITS = {
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"];
@customElement("entity-registry-settings")
@ -140,6 +148,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@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 _submitting?: boolean;
@ -223,6 +241,16 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
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];
if (!deviceClasses) {
@ -358,6 +386,90 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
</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"
? html`<ha-select
.label=${this.hass.localize(
@ -628,6 +740,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
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 {
if (ev.target.value === "") {
return;
@ -730,6 +867,23 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
params.options_domain = "sensor";
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 {
const result = await updateEntityRegistryEntry(
this.hass!,

View File

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

View File

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

View File

@ -811,6 +811,11 @@
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"entity_id": "Entity ID",
"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_classes": {
"binary_sensor": {