mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20221201.0 (#14505)
This commit is contained in:
commit
77a53ffc6c
@ -1,5 +1,5 @@
|
||||
const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const fs = require("fs/promises");
|
||||
const mapStream = require("map-stream");
|
||||
|
||||
const inDirFrontend = "translations/frontend";
|
||||
@ -46,18 +46,21 @@ gulp.task("check-translations-html", function () {
|
||||
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
|
||||
});
|
||||
|
||||
gulp.task("check-all-files-exist", function () {
|
||||
const file = fs.readFileSync(srcMeta, { encoding });
|
||||
gulp.task("check-all-files-exist", async function () {
|
||||
const file = await fs.readFile(srcMeta, { encoding });
|
||||
const meta = JSON.parse(file);
|
||||
const writings = [];
|
||||
Object.keys(meta).forEach((lang) => {
|
||||
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) {
|
||||
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({}));
|
||||
}
|
||||
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) {
|
||||
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({}));
|
||||
}
|
||||
writings.push(
|
||||
fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
|
||||
flag: "wx",
|
||||
}),
|
||||
fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
|
||||
flag: "wx",
|
||||
})
|
||||
);
|
||||
});
|
||||
return Promise.resolve();
|
||||
await Promise.allSettled(writings);
|
||||
});
|
||||
|
||||
gulp.task(
|
||||
|
@ -307,7 +307,8 @@ export class DemoEntityState extends LitElement {
|
||||
html`${computeStateDisplay(
|
||||
hass.localize,
|
||||
entry.stateObj,
|
||||
hass.locale
|
||||
hass.locale,
|
||||
hass.entities
|
||||
)}`,
|
||||
},
|
||||
device_class: {
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20221130.0"
|
||||
version = "20221201.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -7,4 +7,4 @@ set -e
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
# Install node modules
|
||||
yarn install
|
||||
yarn install
|
9
script/setup_translations
Executable file
9
script/setup_translations
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# Setup translation fetching during development
|
||||
|
||||
# Stop on errors
|
||||
set -e
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
./node_modules/.bin/gulp setup-and-fetch-nightly-translations
|
@ -5,6 +5,7 @@ const ALERTING_DEVICE_CLASSES = new Set([
|
||||
"carbon_monoxide",
|
||||
"gas",
|
||||
"heat",
|
||||
"moisture",
|
||||
"problem",
|
||||
"safety",
|
||||
"smoke",
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstallingFromAttributes,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
} from "../../data/update";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
|
||||
import { formatDate } from "../datetime/format_date";
|
||||
import { formatDateTime } from "../datetime/format_date_time";
|
||||
@ -23,11 +25,13 @@ export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): string =>
|
||||
computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
entities,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
state !== undefined ? state : stateObj.state
|
||||
@ -36,6 +40,7 @@ export const computeStateDisplay = (
|
||||
export const computeStateDisplayFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
entities: HomeAssistant["entities"],
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
state: string
|
||||
@ -194,7 +199,13 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
: localize("ui.card.update.up_to_date");
|
||||
}
|
||||
|
||||
const entity = entities[entityId] as EntityRegistryEntry | undefined;
|
||||
|
||||
return (
|
||||
(entity?.translation_key &&
|
||||
localize(
|
||||
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
|
||||
)) ||
|
||||
// Return device class translation
|
||||
(attributes.device_class &&
|
||||
localize(
|
||||
|
@ -1,4 +1,7 @@
|
||||
export const countries = [
|
||||
import memoizeOne from "memoize-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
|
||||
export const COUNTRIES = [
|
||||
"AD",
|
||||
"AE",
|
||||
"AF",
|
||||
@ -250,23 +253,31 @@ export const countries = [
|
||||
"ZW",
|
||||
];
|
||||
|
||||
export const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
export const getCountryOptions = memoizeOne((language?: string) => {
|
||||
const countryDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "region",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const options = COUNTRIES.map((country) => ({
|
||||
value: country,
|
||||
label: countryDisplayNames ? countryDisplayNames.of(country)! : country,
|
||||
}));
|
||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
||||
return options;
|
||||
});
|
||||
|
||||
export const createCountryListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "countries";
|
||||
for (const country of countries) {
|
||||
const options = getCountryOptions();
|
||||
for (const country of options) {
|
||||
const option = document.createElement("option");
|
||||
option.value = country;
|
||||
option.innerText = countryDisplayNames
|
||||
? countryDisplayNames.of(country)!
|
||||
: country;
|
||||
option.value = country.value;
|
||||
option.innerText = country.label;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
|
@ -1,4 +1,7 @@
|
||||
export const currencies = [
|
||||
import memoizeOne from "memoize-one";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
|
||||
export const CURRENCIES = [
|
||||
"AED",
|
||||
"AFN",
|
||||
"ALL",
|
||||
@ -158,23 +161,29 @@ export const currencies = [
|
||||
"ZWL",
|
||||
];
|
||||
|
||||
export const currencyDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(undefined, {
|
||||
type: "currency",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
export const getCurrencyOptions = memoizeOne((language?: string) => {
|
||||
const currencyDisplayNames =
|
||||
Intl && "DisplayNames" in Intl
|
||||
? new Intl.DisplayNames(language, {
|
||||
type: "currency",
|
||||
fallback: "code",
|
||||
})
|
||||
: undefined;
|
||||
const options = CURRENCIES.map((currency) => ({
|
||||
value: currency,
|
||||
label: currencyDisplayNames ? currencyDisplayNames.of(currency)! : currency,
|
||||
}));
|
||||
options.sort((a, b) => caseInsensitiveStringCompare(a.label, b.label));
|
||||
return options;
|
||||
});
|
||||
|
||||
export const createCurrencyListEl = () => {
|
||||
const list = document.createElement("datalist");
|
||||
list.id = "currencies";
|
||||
for (const currency of currencies) {
|
||||
for (const currency of getCurrencyOptions()) {
|
||||
const option = document.createElement("option");
|
||||
option.value = currency;
|
||||
option.innerText = currencyDisplayNames
|
||||
? currencyDisplayNames.of(currency)!
|
||||
: currency;
|
||||
option.value = currency.value;
|
||||
option.innerText = currency.label;
|
||||
list.appendChild(option);
|
||||
}
|
||||
return list;
|
||||
|
@ -55,6 +55,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
key
|
||||
)
|
||||
: formatAttributeValue(this.hass, key),
|
||||
|
@ -158,7 +158,8 @@ export class HaStateLabelBadge extends LitElement {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
entityState,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit";
|
||||
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
|
||||
import type {
|
||||
ComboBoxDataProvider,
|
||||
ComboBoxLight,
|
||||
ComboBoxLightFilterChangedEvent,
|
||||
ComboBoxLightOpenedChangedEvent,
|
||||
@ -82,6 +83,9 @@ export class HaComboBox extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public filteredItems?: any[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public dataProvider?: ComboBoxDataProvider<any>;
|
||||
|
||||
@property({ attribute: "allow-custom-value", type: Boolean })
|
||||
public allowCustomValue = false;
|
||||
|
||||
@ -148,6 +152,7 @@ export class HaComboBox extends LitElement {
|
||||
.items=${this.items}
|
||||
.value=${this.value || ""}
|
||||
.filteredItems=${this.filteredItems}
|
||||
.dataProvider=${this.dataProvider}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@ -225,13 +230,13 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
|
||||
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
|
||||
ev.stopPropagation();
|
||||
const opened = ev.detail.value;
|
||||
// delay this so we can handle click event for toggle button before setting _opened
|
||||
setTimeout(() => {
|
||||
this.opened = opened;
|
||||
}, 0);
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail);
|
||||
fireEvent(this, "opened-changed", { value: ev.detail.value });
|
||||
|
||||
if (opened) {
|
||||
const overlay = document.querySelector<HTMLElement>(
|
||||
@ -300,8 +305,8 @@ export class HaComboBox extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
|
||||
// @ts-ignore
|
||||
fireEvent(this, ev.type, ev.detail, { composed: false });
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "filter-changed", { value: ev.detail.value });
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
|
||||
@ -363,3 +368,10 @@ declare global {
|
||||
"ha-combo-box": HaComboBox;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"filter-changed": { value: string };
|
||||
"opened-changed": { value: boolean };
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ export class HaDateInput extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
iconTrailing
|
||||
helperPersistent
|
||||
readonly
|
||||
@click=${this._openDialog}
|
||||
.value=${this.value
|
||||
? formatDateNumeric(new Date(this.value), this.locale)
|
||||
|
@ -1,28 +1,76 @@
|
||||
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import {
|
||||
ComboBoxDataProviderCallback,
|
||||
ComboBoxDataProviderParams,
|
||||
} from "@vaadin/combo-box/vaadin-combo-box-light";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import { PolymerChangedEvent } from "../polymer-types";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon";
|
||||
|
||||
type IconItem = {
|
||||
icon: string;
|
||||
parts: Set<string>;
|
||||
keywords: string[];
|
||||
};
|
||||
let iconItems: IconItem[] = [];
|
||||
let iconLoaded = false;
|
||||
|
||||
// eslint-disable-next-line lit/prefer-static-styles
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<mwc-list-item
|
||||
graphic="avatar"
|
||||
>
|
||||
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
|
||||
${item.icon}
|
||||
</mwc-list-item>`;
|
||||
type RankedIcon = {
|
||||
icon: string;
|
||||
rank: number;
|
||||
};
|
||||
|
||||
let ICONS: IconItem[] = [];
|
||||
let ICONS_LOADED = false;
|
||||
|
||||
const loadIcons = async () => {
|
||||
ICONS_LOADED = true;
|
||||
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
ICONS = iconList.default.map((icon) => ({
|
||||
icon: `mdi:${icon.name}`,
|
||||
parts: new Set(icon.name.split("-")),
|
||||
keywords: icon.keywords,
|
||||
}));
|
||||
|
||||
const customIconLoads: Promise<IconItem[]>[] = [];
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
customIconLoads.push(loadCustomIconItems(iconSet));
|
||||
});
|
||||
(await Promise.all(customIconLoads)).forEach((customIconItems) => {
|
||||
ICONS.push(...customIconItems);
|
||||
});
|
||||
};
|
||||
|
||||
const loadCustomIconItems = async (iconsetPrefix: string) => {
|
||||
try {
|
||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||
if (typeof getIconList !== "function") {
|
||||
return [];
|
||||
}
|
||||
const iconList = await getIconList();
|
||||
const customIconItems = iconList.map((icon) => ({
|
||||
icon: `${iconsetPrefix}:${icon.name}`,
|
||||
parts: new Set(icon.name.split("-")),
|
||||
keywords: icon.keywords ?? [],
|
||||
}));
|
||||
return customIconItems;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<IconItem | RankedIcon> = (item) =>
|
||||
html`<mwc-list-item graphic="avatar">
|
||||
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
|
||||
${item.icon}
|
||||
</mwc-list-item>`;
|
||||
|
||||
@customElement("ha-icon-picker")
|
||||
export class HaIconPicker extends LitElement {
|
||||
@ -46,10 +94,6 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@query("ha-combo-box", true) private comboBox!: HaComboBox;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-combo-box
|
||||
@ -58,7 +102,7 @@ export class HaIconPicker extends LitElement {
|
||||
item-label-path="icon"
|
||||
.value=${this._value}
|
||||
allow-custom-value
|
||||
.filteredItems=${iconItems}
|
||||
.dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@ -70,7 +114,6 @@ export class HaIconPicker extends LitElement {
|
||||
icon
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
${this._value || this.placeholder
|
||||
? html`
|
||||
@ -87,46 +130,55 @@ export class HaIconPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
this._opened = ev.detail.value;
|
||||
if (this._opened && !iconLoaded) {
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
|
||||
iconItems = iconList.default.map((icon) => ({
|
||||
icon: `mdi:${icon.name}`,
|
||||
keywords: icon.keywords,
|
||||
}));
|
||||
iconLoaded = true;
|
||||
|
||||
this.comboBox.filteredItems = iconItems;
|
||||
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
this._loadCustomIconItems(iconSet);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadCustomIconItems(iconsetPrefix: string) {
|
||||
try {
|
||||
const getIconList = customIcons[iconsetPrefix].getIconList;
|
||||
if (typeof getIconList !== "function") {
|
||||
return;
|
||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||
private _filterIcons = memoizeOne(
|
||||
(filter: string, iconItems: IconItem[] = ICONS) => {
|
||||
if (!filter) {
|
||||
return iconItems;
|
||||
}
|
||||
const iconList = await getIconList();
|
||||
const customIconItems = iconList.map((icon) => ({
|
||||
icon: `${iconsetPrefix}:${icon.name}`,
|
||||
keywords: icon.keywords ?? [],
|
||||
}));
|
||||
iconItems.push(...customIconItems);
|
||||
this.comboBox.filteredItems = iconItems;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line
|
||||
console.warn(`Unable to load icon list for ${iconsetPrefix} iconset`);
|
||||
}
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues) {
|
||||
return !this._opened || changedProps.has("_opened");
|
||||
const filteredItems: RankedIcon[] = [];
|
||||
const addIcon = (icon: string, rank: number) =>
|
||||
filteredItems.push({ icon, rank });
|
||||
|
||||
// Filter and rank such that exact matches rank higher, and prefer icon name matches over keywords
|
||||
for (const item of iconItems) {
|
||||
if (item.parts.has(filter)) {
|
||||
addIcon(item.icon, 1);
|
||||
} else if (item.keywords.includes(filter)) {
|
||||
addIcon(item.icon, 2);
|
||||
} else if (item.icon.includes(filter)) {
|
||||
addIcon(item.icon, 3);
|
||||
} else if (item.keywords.some((word) => word.includes(filter))) {
|
||||
addIcon(item.icon, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (filteredItems.length === 0) {
|
||||
addIcon(filter, 0);
|
||||
}
|
||||
|
||||
return filteredItems.sort((itemA, itemB) => itemA.rank - itemB.rank);
|
||||
}
|
||||
);
|
||||
|
||||
private _iconProvider = (
|
||||
params: ComboBoxDataProviderParams,
|
||||
callback: ComboBoxDataProviderCallback<IconItem | RankedIcon>
|
||||
) => {
|
||||
const filteredItems = this._filterIcons(params.filter.toLowerCase(), ICONS);
|
||||
const iStart = params.page * params.pageSize;
|
||||
const iEnd = iStart + params.pageSize;
|
||||
callback(filteredItems.slice(iStart, iEnd), filteredItems.length);
|
||||
};
|
||||
|
||||
private async _openedChanged(ev: PolymerChangedEvent<boolean>) {
|
||||
const opened = ev.detail.value;
|
||||
if (opened && !ICONS_LOADED) {
|
||||
await loadIcons();
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: PolymerChangedEvent<string>) {
|
||||
@ -147,35 +199,6 @@ export class HaIconPicker extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
const characterCount = filterString.length;
|
||||
if (characterCount >= 2) {
|
||||
const filteredItems: IconItem[] = [];
|
||||
const filteredItemsByKeywords: IconItem[] = [];
|
||||
|
||||
iconItems.forEach((item) => {
|
||||
if (item.icon.includes(filterString)) {
|
||||
filteredItems.push(item);
|
||||
return;
|
||||
}
|
||||
if (item.keywords.some((t) => t.includes(filterString))) {
|
||||
filteredItemsByKeywords.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
filteredItems.push(...filteredItemsByKeywords);
|
||||
|
||||
if (filteredItems.length > 0) {
|
||||
this.comboBox.filteredItems = filteredItems;
|
||||
} else {
|
||||
this.comboBox.filteredItems = [{ icon: filterString, keywords: [] }];
|
||||
}
|
||||
} else {
|
||||
this.comboBox.filteredItems = iconItems;
|
||||
}
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
@ -85,7 +85,12 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_localizeState(stateObj) {
|
||||
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
|
||||
return computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-water_heater-state", HaWaterHeaterState);
|
||||
|
@ -44,7 +44,7 @@ export class HaTileInfo extends LitElement {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--secondary-text-color);
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ export interface EntityRegistryEntry {
|
||||
has_entity_name: boolean;
|
||||
original_name?: string;
|
||||
unique_id: string;
|
||||
translation_key?: string;
|
||||
}
|
||||
|
||||
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
|
||||
|
@ -184,6 +184,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
|
||||
const processTimelineEntity = (
|
||||
localize: LocalizeFunc,
|
||||
language: FrontendLocaleData,
|
||||
entities: HomeAssistant["entities"],
|
||||
entityId: string,
|
||||
states: EntityHistoryState[],
|
||||
current_state: HassEntity | undefined
|
||||
@ -198,6 +199,7 @@ const processTimelineEntity = (
|
||||
state_localize: computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
language,
|
||||
entities,
|
||||
entityId,
|
||||
state.a || first.a,
|
||||
state.s
|
||||
@ -344,6 +346,7 @@ export const computeHistory = (
|
||||
processTimelineEntity(
|
||||
localize,
|
||||
hass.locale,
|
||||
hass.entities,
|
||||
entityId,
|
||||
stateInfo,
|
||||
currentState
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||
import { LocalizeFunc } from "../common/translations/localize";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
|
||||
export type IntegrationType =
|
||||
| "device"
|
||||
@ -23,6 +25,7 @@ export interface IntegrationManifest {
|
||||
zeroconf?: string[];
|
||||
homekit?: { models: string[] };
|
||||
integration_type?: IntegrationType;
|
||||
loggers?: string[];
|
||||
quality_scale?: "gold" | "internal" | "platinum" | "silver";
|
||||
iot_class:
|
||||
| "assumed_state"
|
||||
@ -36,6 +39,24 @@ export interface IntegrationSetup {
|
||||
seconds?: number;
|
||||
}
|
||||
|
||||
export interface IntegrationLogInfo {
|
||||
domain: string;
|
||||
level?: number;
|
||||
}
|
||||
|
||||
export enum LogSeverity {
|
||||
CRITICAL = 50,
|
||||
FATAL = 50,
|
||||
ERROR = 40,
|
||||
WARNING = 30,
|
||||
WARN = 30,
|
||||
INFO = 20,
|
||||
DEBUG = 10,
|
||||
NOTSET = 0,
|
||||
}
|
||||
|
||||
export type IntegrationLogPersistance = "none" | "once" | "permanent";
|
||||
|
||||
export const integrationIssuesUrl = (
|
||||
domain: string,
|
||||
manifest: IntegrationManifest
|
||||
@ -69,3 +90,46 @@ export const fetchIntegrationManifest = (
|
||||
|
||||
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
|
||||
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
|
||||
|
||||
export const fetchIntegrationLogInfo = (conn) =>
|
||||
conn.sendMessagePromise({
|
||||
type: "logger/log_info",
|
||||
});
|
||||
|
||||
export const setIntegrationLogLevel = (
|
||||
hass: HomeAssistant,
|
||||
integration: string,
|
||||
level: string,
|
||||
persistence: IntegrationLogPersistance
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "logger/integration_log_level",
|
||||
integration,
|
||||
level,
|
||||
persistence,
|
||||
});
|
||||
|
||||
const subscribeLogInfoUpdates = (conn, store) =>
|
||||
conn.subscribeEvents(
|
||||
debounce(
|
||||
() =>
|
||||
fetchIntegrationLogInfo(conn).then((log_infos) =>
|
||||
store.setState(log_infos, true)
|
||||
),
|
||||
200,
|
||||
true
|
||||
),
|
||||
"logging_changed"
|
||||
);
|
||||
|
||||
export const subscribeLogInfo = (
|
||||
conn: Connection,
|
||||
onChange: (devices: IntegrationLogInfo[]) => void
|
||||
) =>
|
||||
createCollection<IntegrationLogInfo[]>(
|
||||
"_integration_log_info",
|
||||
fetchIntegrationLogInfo,
|
||||
subscribeLogInfoUpdates,
|
||||
conn,
|
||||
onChange
|
||||
);
|
||||
|
@ -435,7 +435,13 @@ export const localizeStateMessage = (
|
||||
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
|
||||
"state",
|
||||
stateObj
|
||||
? computeStateDisplay(localize, stateObj, hass.locale, state)
|
||||
? computeStateDisplay(
|
||||
localize,
|
||||
stateObj,
|
||||
hass.locale,
|
||||
hass.entities,
|
||||
state
|
||||
)
|
||||
: state
|
||||
);
|
||||
};
|
||||
|
@ -90,7 +90,12 @@ export const computeDisplayTimer = (
|
||||
}
|
||||
|
||||
if (stateObj.state === "idle" || timeRemaining === 0) {
|
||||
return computeStateDisplay(hass.localize, stateObj, hass.locale);
|
||||
return computeStateDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.locale,
|
||||
hass.entities
|
||||
);
|
||||
}
|
||||
|
||||
let display = secondsToDuration(timeRemaining || 0);
|
||||
@ -99,7 +104,8 @@ export const computeDisplayTimer = (
|
||||
display = `${display} (${computeStateDisplay(
|
||||
hass.localize,
|
||||
stateObj,
|
||||
hass.locale
|
||||
hass.locale,
|
||||
hass.entities
|
||||
)})`;
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ declare global {
|
||||
export type TranslationCategory =
|
||||
| "title"
|
||||
| "state"
|
||||
| "entity"
|
||||
| "config"
|
||||
| "config_panel"
|
||||
| "options"
|
||||
|
@ -37,7 +37,8 @@ export class HuiConfiguratorNotificationItem extends LitElement {
|
||||
>${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this.notification,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}</mwc-button
|
||||
>
|
||||
</notification-item-template>
|
||||
|
@ -307,7 +307,9 @@ export const provideHass = (
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
areas: {},
|
||||
devices: {},
|
||||
entities: {},
|
||||
...overrideData,
|
||||
};
|
||||
|
||||
|
@ -137,6 +137,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
|
||||
super.hassConnected();
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "state");
|
||||
// @ts-ignore
|
||||
this._loadHassTranslations(this.hass!.language, "entity");
|
||||
|
||||
document.addEventListener(
|
||||
"visibilitychange",
|
||||
|
@ -1,6 +1,6 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiCalendarClock, mdiClose } from "@mdi/js";
|
||||
import { isSameDay } from "date-fns/esm";
|
||||
import { addDays, isSameDay } from "date-fns/esm";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { RRule } from "rrule";
|
||||
@ -134,7 +134,10 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
|
||||
private _formatDateRange() {
|
||||
const start = new Date(this._data!.dtstart);
|
||||
const end = new Date(this._data!.dtend);
|
||||
// All day events should be displayed as a day earlier
|
||||
const end = isDate(this._data.dtend)
|
||||
? addDays(new Date(this._data!.dtend), -1)
|
||||
: new Date(this._data!.dtend);
|
||||
// The range can be shortened when the start and end are on the same day.
|
||||
if (isSameDay(start, end)) {
|
||||
if (isDate(this._data.dtstart)) {
|
||||
@ -148,10 +151,15 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
)} - ${formatTime(end, this.hass.locale)}`;
|
||||
}
|
||||
// An event across multiple dates, optionally with a time range
|
||||
return `${formatDateTime(start, this.hass.locale)} - ${formatDateTime(
|
||||
end,
|
||||
this.hass.locale
|
||||
)}`;
|
||||
return `${
|
||||
isDate(this._data.dtstart)
|
||||
? formatDate(start, this.hass.locale)
|
||||
: formatDateTime(start, this.hass.locale)
|
||||
} - ${
|
||||
isDate(this._data.dtend)
|
||||
? formatDate(end, this.hass.locale)
|
||||
: formatDateTime(end, this.hass.locale)
|
||||
}`;
|
||||
}
|
||||
|
||||
private async _editEvent() {
|
||||
|
@ -4,6 +4,7 @@ import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||
import { addDays, addHours, startOfHour } from "date-fns/esm";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isDate } from "../../common/string/is_date";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-time-input";
|
||||
@ -39,7 +40,9 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _calendarId?: string;
|
||||
|
||||
@state() private _data?: CalendarEventMutableParams;
|
||||
@state() private _summary = "";
|
||||
|
||||
@state() private _rrule?: string;
|
||||
|
||||
@state() private _allDay = false;
|
||||
|
||||
@ -49,40 +52,30 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: CalendarEventEditDialogParams
|
||||
): Promise<void> {
|
||||
public showDialog(params: CalendarEventEditDialogParams): void {
|
||||
this._error = undefined;
|
||||
this._params = params;
|
||||
this._calendars = params.calendars;
|
||||
this._calendarId = params.calendarId || this._calendars[0].entity_id;
|
||||
if (params.entry) {
|
||||
const entry = params.entry!;
|
||||
this._data = entry;
|
||||
this._allDay = isDate(entry.dtstart);
|
||||
this._summary = entry.summary;
|
||||
this._rrule = entry.rrule;
|
||||
if (this._allDay) {
|
||||
this._dtstart = new Date(entry.dtstart);
|
||||
// Calendar event end dates are exclusive, but not shown that way in the UI. The
|
||||
// reverse happens when persisting the event.
|
||||
this._dtend = new Date(entry.dtend);
|
||||
this._dtend.setDate(this._dtend.getDate() - 1);
|
||||
this._dtend = addDays(new Date(entry.dtend), -1);
|
||||
} else {
|
||||
this._dtstart = new Date(entry.dtstart);
|
||||
this._dtend = new Date(entry.dtend);
|
||||
}
|
||||
} else {
|
||||
this._data = {
|
||||
summary: "",
|
||||
// Dates are set in _dateChanged()
|
||||
dtstart: "",
|
||||
dtend: "",
|
||||
};
|
||||
this._allDay = false;
|
||||
this._dtstart = startOfHour(new Date());
|
||||
this._dtend = addHours(this._dtstart, 1);
|
||||
this._dateChanged();
|
||||
}
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@ -90,6 +83,12 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
const isCreate = this._params.entry === undefined;
|
||||
|
||||
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
|
||||
this._dtstart,
|
||||
this._dtend
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@ -100,7 +99,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
<div class="header_title">
|
||||
${isCreate
|
||||
? this.hass.localize("ui.components.calendar.event.add")
|
||||
: this._data!.summary}
|
||||
: this._summary}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||
@ -155,13 +154,13 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
>
|
||||
<div class="flex">
|
||||
<ha-date-input
|
||||
.value=${this._data!.dtstart}
|
||||
.value=${startDate}
|
||||
.locale=${this.hass.locale}
|
||||
@value-changed=${this._startDateChanged}
|
||||
></ha-date-input>
|
||||
${!this._allDay
|
||||
? html`<ha-time-input
|
||||
.value=${this._data!.dtstart.split("T")[1]}
|
||||
.value=${startTime}
|
||||
.locale=${this.hass.locale}
|
||||
@value-changed=${this._startTimeChanged}
|
||||
></ha-time-input>`
|
||||
@ -174,14 +173,14 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
>
|
||||
<div class="flex">
|
||||
<ha-date-input
|
||||
.value=${this._data!.dtend}
|
||||
.min=${this._data!.dtstart}
|
||||
.value=${endDate}
|
||||
.min=${startDate}
|
||||
.locale=${this.hass.locale}
|
||||
@value-changed=${this._endDateChanged}
|
||||
></ha-date-input>
|
||||
${!this._allDay
|
||||
? html`<ha-time-input
|
||||
.value=${this._data!.dtend.split("T")[1]}
|
||||
.value=${endTime}
|
||||
.locale=${this.hass.locale}
|
||||
@value-changed=${this._endTimeChanged}
|
||||
></ha-time-input>`
|
||||
@ -190,7 +189,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
</div>
|
||||
<ha-recurrence-rule-editor
|
||||
.locale=${this.hass.locale}
|
||||
.value=${this._data!.rrule || ""}
|
||||
.value=${this._rrule || ""}
|
||||
@value-changed=${this._handleRRuleChanged}
|
||||
>
|
||||
</ha-recurrence-rule-editor>
|
||||
@ -230,57 +229,78 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _getLocaleStrings = memoizeOne((startDate?: Date, endDate?: Date) =>
|
||||
// en-CA locale used for date format YYYY-MM-DD
|
||||
// en-GB locale used for 24h time format HH:MM:SS
|
||||
{
|
||||
const timeZone = this.hass.config.time_zone;
|
||||
return {
|
||||
startDate: startDate?.toLocaleDateString("en-CA", { timeZone }),
|
||||
startTime: startDate?.toLocaleTimeString("en-GB", { timeZone }),
|
||||
endDate: endDate?.toLocaleDateString("en-CA", { timeZone }),
|
||||
endTime: endDate?.toLocaleTimeString("en-GB", { timeZone }),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
private _handleSummaryChanged(ev) {
|
||||
this._data!.summary = ev.target.value;
|
||||
this._summary = ev.target.value;
|
||||
}
|
||||
|
||||
private _handleRRuleChanged(ev) {
|
||||
this._data!.rrule = ev.detail.value;
|
||||
this.requestUpdate();
|
||||
this._rrule = ev.detail.value;
|
||||
}
|
||||
|
||||
private _allDayToggleChanged(ev) {
|
||||
this._allDay = ev.target.checked;
|
||||
this._dateChanged();
|
||||
}
|
||||
|
||||
private _startDateChanged(ev: CustomEvent) {
|
||||
this._dtstart = new Date(
|
||||
ev.detail.value + "T" + this._dtstart!.toISOString().split("T")[1]
|
||||
);
|
||||
this._dateChanged();
|
||||
}
|
||||
|
||||
private _endDateChanged(ev: CustomEvent) {
|
||||
this._dtend = new Date(
|
||||
ev.detail.value + "T" + this._dtend!.toISOString().split("T")[1]
|
||||
);
|
||||
this._dateChanged();
|
||||
}
|
||||
|
||||
private _startTimeChanged(ev: CustomEvent) {
|
||||
this._dtstart = new Date(
|
||||
this._dtstart!.toISOString().split("T")[0] + "T" + ev.detail.value
|
||||
);
|
||||
this._dateChanged();
|
||||
}
|
||||
|
||||
private _endTimeChanged(ev: CustomEvent) {
|
||||
this._dtend = new Date(
|
||||
this._dtend!.toISOString().split("T")[0] + "T" + ev.detail.value
|
||||
);
|
||||
this._dateChanged();
|
||||
}
|
||||
|
||||
private _dateChanged() {
|
||||
private _calculateData() {
|
||||
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
|
||||
this._dtstart,
|
||||
this._dtend
|
||||
);
|
||||
const data: CalendarEventMutableParams = {
|
||||
summary: this._summary,
|
||||
rrule: this._rrule,
|
||||
dtstart: "",
|
||||
dtend: "",
|
||||
};
|
||||
if (this._allDay) {
|
||||
this._data!.dtstart = this._dtstart!.toISOString();
|
||||
data.dtstart = startDate!;
|
||||
// End date/time is exclusive when persisted
|
||||
this._data!.dtend = addDays(new Date(this._dtend!), 1).toISOString();
|
||||
data.dtend = addDays(new Date(this._dtend!), 1).toLocaleDateString(
|
||||
"en-CA"
|
||||
);
|
||||
} else {
|
||||
this._data!.dtstart = this._dtstart!.toISOString();
|
||||
this._data!.dtend = this._dtend!.toISOString();
|
||||
data.dtstart = `${startDate}T${startTime}`;
|
||||
data.dtend = `${endDate}T${endTime}`;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private _handleCalendarChanged(ev: CustomEvent) {
|
||||
@ -290,7 +310,11 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
private async _createEvent() {
|
||||
this._submitting = true;
|
||||
try {
|
||||
await createCalendarEvent(this.hass!, this._calendarId!, this._data!);
|
||||
await createCalendarEvent(
|
||||
this.hass!,
|
||||
this._calendarId!,
|
||||
this._calculateData()
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._error = err ? err.message : "Unknown error";
|
||||
} finally {
|
||||
@ -358,9 +382,10 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
this._calendars = [];
|
||||
this._calendarId = undefined;
|
||||
this._params = undefined;
|
||||
this._data = undefined;
|
||||
this._dtstart = undefined;
|
||||
this._dtend = undefined;
|
||||
this._summary = "";
|
||||
this._rrule = undefined;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
@ -53,19 +53,22 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (!changedProps.has("value") && !changedProps.has("locale")) {
|
||||
if (changedProps.has("locale")) {
|
||||
this._allWeekdays = getWeekdays(firstWeekdayIndex(this.locale)).map(
|
||||
(day: Weekday) => day.toString() as WeekdayStr
|
||||
);
|
||||
}
|
||||
|
||||
if (!changedProps.has("value") || this._computedRRule === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._interval = 1;
|
||||
this._weekday.clear();
|
||||
this._end = "never";
|
||||
this._count = undefined;
|
||||
this._until = undefined;
|
||||
|
||||
this._allWeekdays = getWeekdays(firstWeekdayIndex(this.locale)).map(
|
||||
(day: Weekday) => day.toString() as WeekdayStr
|
||||
);
|
||||
|
||||
this._computedRRule = this.value;
|
||||
if (this.value === "") {
|
||||
this._freq = "none";
|
||||
@ -274,6 +277,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _onUntilChange(e: CustomEvent) {
|
||||
e.stopPropagation();
|
||||
this._until = new Date(e.detail.value);
|
||||
this._updateRule();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// and the values defined by rrule.js.
|
||||
import { RRule, Frequency, Weekday } from "rrule";
|
||||
import type { WeekdayStr } from "rrule";
|
||||
import { addDays, addMonths, addWeeks, addYears } from "date-fns";
|
||||
|
||||
export type RepeatFrequency =
|
||||
| "none"
|
||||
@ -35,14 +36,14 @@ export function untilValue(freq: RepeatFrequency): Date {
|
||||
const increment = DEFAULT_COUNT[freq];
|
||||
switch (freq) {
|
||||
case "yearly":
|
||||
return new Date(new Date().setFullYear(today.getFullYear() + increment));
|
||||
return addYears(today, increment);
|
||||
case "monthly":
|
||||
return new Date(new Date().setMonth(today.getMonth() + increment));
|
||||
return addMonths(today, increment);
|
||||
case "weekly":
|
||||
return new Date(new Date().setDate(today.getDate() + 7 * increment));
|
||||
return addWeeks(today, increment);
|
||||
case "daily":
|
||||
default:
|
||||
return new Date(new Date().setDate(today.getDate() + increment));
|
||||
return addDays(today, increment);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,16 +6,11 @@ import memoizeOne from "memoize-one";
|
||||
import { UNIT_C } from "../../../common/const";
|
||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/buttons/ha-progress-button";
|
||||
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
|
||||
import {
|
||||
countries,
|
||||
countryDisplayNames,
|
||||
} from "../../../components/country-datalist";
|
||||
import {
|
||||
currencies,
|
||||
currencyDisplayNames,
|
||||
} from "../../../components/currency-datalist";
|
||||
import { getCountryOptions } from "../../../components/country-datalist";
|
||||
import { getCurrencyOptions } from "../../../components/currency-datalist";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-formfield";
|
||||
import "../../../components/ha-radio";
|
||||
@ -55,6 +50,8 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
|
||||
@state() private _location?: [number, number];
|
||||
|
||||
@state() private _languages?: { value: string; label: string }[];
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canEdit = ["storage", "default"].includes(
|
||||
this.hass.config.config_source
|
||||
@ -187,13 +184,11 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${currencies.map(
|
||||
(currency) =>
|
||||
html`<mwc-list-item .value=${currency}
|
||||
>${currencyDisplayNames
|
||||
? currencyDisplayNames.of(currency)
|
||||
: currency}</mwc-list-item
|
||||
>`
|
||||
${getCurrencyOptions(this.hass.locale.language).map(
|
||||
({ value, label }) =>
|
||||
html`<mwc-list-item .value=${value}>
|
||||
${label}
|
||||
</mwc-list-item>`
|
||||
)}</ha-select
|
||||
>
|
||||
<a
|
||||
@ -218,13 +213,11 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${countries.map(
|
||||
(country) =>
|
||||
html`<mwc-list-item .value=${country}
|
||||
>${countryDisplayNames
|
||||
? countryDisplayNames.of(country)
|
||||
: country}</mwc-list-item
|
||||
>`
|
||||
${getCountryOptions(this.hass.locale.language).map(
|
||||
({ value, label }) =>
|
||||
html`<mwc-list-item .value=${value}>
|
||||
${label}
|
||||
</mwc-list-item>`
|
||||
)}</ha-select
|
||||
>
|
||||
<ha-select
|
||||
@ -239,12 +232,10 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
@closed=${stopPropagation}
|
||||
@change=${this._handleChange}
|
||||
>
|
||||
${Object.entries(
|
||||
this.hass.translationMetadata.translations
|
||||
).map(
|
||||
([code, metadata]) =>
|
||||
html`<mwc-list-item .value=${code}
|
||||
>${metadata.nativeName}</mwc-list-item
|
||||
${this._languages?.map(
|
||||
({ value, label }) =>
|
||||
html`<mwc-list-item .value=${value}
|
||||
>${label}</mwc-list-item
|
||||
>`
|
||||
)}</ha-select
|
||||
>
|
||||
@ -300,6 +291,21 @@ class HaConfigSectionGeneral extends LitElement {
|
||||
this._elevation = this.hass.config.elevation;
|
||||
this._timeZone = this.hass.config.time_zone;
|
||||
this._name = this.hass.config.location_name;
|
||||
this._computeLanguages();
|
||||
}
|
||||
|
||||
private _computeLanguages() {
|
||||
if (!this.hass.translationMetadata?.translations) {
|
||||
return;
|
||||
}
|
||||
this._languages = Object.entries(this.hass.translationMetadata.translations)
|
||||
.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a[1].nativeName, b[1].nativeName)
|
||||
)
|
||||
.map(([value, metaData]) => ({
|
||||
value,
|
||||
label: metaData.nativeName,
|
||||
}));
|
||||
}
|
||||
|
||||
private _handleChange(ev) {
|
||||
|
@ -51,6 +51,8 @@ import {
|
||||
fetchIntegrationManifest,
|
||||
fetchIntegrationManifests,
|
||||
IntegrationManifest,
|
||||
IntegrationLogInfo,
|
||||
subscribeLogInfo,
|
||||
} from "../../../data/integration";
|
||||
import {
|
||||
getIntegrationDescriptions,
|
||||
@ -154,6 +156,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _diagnosticHandlers?: Record<string, boolean>;
|
||||
|
||||
@state() private _logInfos?: {
|
||||
[integration: string]: IntegrationLogInfo;
|
||||
};
|
||||
|
||||
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection, (entries) => {
|
||||
@ -230,6 +236,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
{ type: ["device", "hub", "service"] }
|
||||
),
|
||||
subscribeLogInfo(this.hass.connection, (log_infos) => {
|
||||
const logInfoLookup: { [integration: string]: IntegrationLogInfo } = {};
|
||||
for (const log_info of log_infos) {
|
||||
logInfoLookup[log_info.domain] = log_info;
|
||||
}
|
||||
this._logInfos = logInfoLookup;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
@ -514,6 +527,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
|
||||
.supportsDiagnostics=${this._diagnosticHandlers
|
||||
? this._diagnosticHandlers[domain]
|
||||
: false}
|
||||
.logInfo=${this._logInfos ? this._logInfos[domain] : null}
|
||||
></ha-integration-card>`
|
||||
)
|
||||
: this._filter &&
|
||||
|
@ -5,6 +5,8 @@ import {
|
||||
mdiAlertCircle,
|
||||
mdiBookshelf,
|
||||
mdiBug,
|
||||
mdiBugPlay,
|
||||
mdiBugStop,
|
||||
mdiChevronLeft,
|
||||
mdiCog,
|
||||
mdiDelete,
|
||||
@ -47,11 +49,17 @@ import {
|
||||
ERROR_STATES,
|
||||
RECOVERABLE_STATES,
|
||||
} from "../../../data/config_entries";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import { integrationIssuesUrl } from "../../../data/integration";
|
||||
import {
|
||||
integrationIssuesUrl,
|
||||
IntegrationLogInfo,
|
||||
LogSeverity,
|
||||
setIntegrationLogLevel,
|
||||
} from "../../../data/integration";
|
||||
import { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
|
||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import {
|
||||
@ -95,6 +103,8 @@ export class HaIntegrationCard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public supportsDiagnostics = false;
|
||||
|
||||
@property() public logInfo?: IntegrationLogInfo;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let item = this._selectededConfigEntry;
|
||||
|
||||
@ -137,6 +147,8 @@ export class HaIntegrationCard extends LitElement {
|
||||
.localizedDomainName=${item ? item.localized_domain_name : undefined}
|
||||
.manifest=${this.manifest}
|
||||
.configEntry=${item}
|
||||
.debugLoggingEnabled=${this.logInfo &&
|
||||
this.logInfo.level === LogSeverity.DEBUG}
|
||||
>
|
||||
${this.items.length > 1
|
||||
? html`
|
||||
@ -398,6 +410,28 @@ export class HaIntegrationCard extends LitElement {
|
||||
</mwc-list-item>
|
||||
</a>`
|
||||
: ""}
|
||||
${this.logInfo
|
||||
? html`<mwc-list-item
|
||||
@request-selected=${this.logInfo.level === LogSeverity.DEBUG
|
||||
? this._handleDisableDebugLogging
|
||||
: this._handleEnableDebugLogging}
|
||||
graphic="icon"
|
||||
>
|
||||
${this.logInfo.level === LogSeverity.DEBUG
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.disable_debug_logging"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.enable_debug_logging"
|
||||
)}
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${this.logInfo.level === LogSeverity.DEBUG
|
||||
? mdiBugStop
|
||||
: mdiBugPlay}
|
||||
></ha-svg-icon>
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
${this.manifest &&
|
||||
(this.manifest.is_built_in ||
|
||||
this.manifest.issue_tracker ||
|
||||
@ -501,6 +535,34 @@ export class HaIntegrationCard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleEnableDebugLogging(ev: MouseEvent) {
|
||||
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
|
||||
.configEntry;
|
||||
const integration = configEntry.domain;
|
||||
await setIntegrationLogLevel(
|
||||
this.hass,
|
||||
integration,
|
||||
LogSeverity[LogSeverity.DEBUG],
|
||||
"once"
|
||||
);
|
||||
}
|
||||
|
||||
private async _handleDisableDebugLogging(ev: MouseEvent) {
|
||||
const configEntry = ((ev.target as HTMLElement).closest("ha-card") as any)
|
||||
.configEntry;
|
||||
const integration = configEntry.domain;
|
||||
await setIntegrationLogLevel(
|
||||
this.hass,
|
||||
integration,
|
||||
LogSeverity[LogSeverity.NOTSET],
|
||||
"once"
|
||||
);
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const logFileName = `home-assistant_${integration}_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, getErrorLogDownloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private get _selectededConfigEntry(): ConfigEntryExtended | undefined {
|
||||
return this.items.length === 1
|
||||
? this.items[0]
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
|
||||
import { mdiBugPlay, mdiCloud, mdiPackageVariant, mdiSyncOff } from "@mdi/js";
|
||||
import "@polymer/paper-tooltip/paper-tooltip";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
@ -24,6 +24,8 @@ export class HaIntegrationHeader extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public configEntry?: ConfigEntry;
|
||||
|
||||
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
let primary: string;
|
||||
let secondary: string | undefined;
|
||||
@ -76,6 +78,15 @@ export class HaIntegrationHeader extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.debugLoggingEnabled) {
|
||||
icons.push([
|
||||
mdiBugPlay,
|
||||
this.hass.localize(
|
||||
"ui.panel.config.integrations.config_entry.debug_logging_enabled"
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return html`
|
||||
${!this.banner ? "" : html`<div class="banner">${this.banner}</div>`}
|
||||
<slot name="above-header"></slot>
|
||||
|
@ -205,7 +205,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</span>`
|
||||
: ""}
|
||||
|
@ -167,7 +167,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
||||
: computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}</span
|
||||
>${showUnit
|
||||
? html`
|
||||
|
@ -335,7 +335,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
|
@ -160,7 +160,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
|
@ -121,7 +121,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
|
||||
const entityState = computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
let footer: TemplateResult | string = "";
|
||||
|
@ -255,7 +255,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
||||
title=${`${computeStateName(stateObj)} : ${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
)}`}
|
||||
>
|
||||
<ha-state-icon
|
||||
@ -277,7 +278,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
|
@ -201,7 +201,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
|
||||
return computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -221,7 +221,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
|
||||
${computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</div>
|
||||
<div class="name" .title=${name}>${name}</div>
|
||||
|
@ -83,7 +83,12 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
|
||||
)}
|
||||
>
|
||||
${this._config.prefix}${!this._config.attribute
|
||||
? computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)
|
||||
? computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)
|
||||
: stateObj.attributes[this._config.attribute]}${this._config.suffix}
|
||||
</div>
|
||||
`;
|
||||
|
@ -69,7 +69,8 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow {
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
|
@ -100,6 +100,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
stateObj.state
|
||||
)}
|
||||
</span>
|
||||
|
@ -193,7 +193,12 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.secondaryText=${mediaDescription ||
|
||||
computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)}
|
||||
computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
>
|
||||
<div class="controls">
|
||||
${supportsFeature(stateObj, SUPPORT_TURN_ON) &&
|
||||
|
@ -104,6 +104,7 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow {
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
stateObj.state
|
||||
)}
|
||||
</span>
|
||||
|
@ -83,7 +83,8 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</div>
|
||||
</hui-generic-entity-row>
|
||||
|
@ -49,7 +49,12 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
${computeStateDisplay(this.hass!.localize, stateObj, this.hass.locale)}
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
@ -64,7 +64,8 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow {
|
||||
${computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
stateObj,
|
||||
this.hass!.locale
|
||||
this.hass!.locale,
|
||||
this.hass!.entities
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
|
@ -120,7 +120,8 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
|
||||
? computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)
|
||||
: html`
|
||||
${formatNumber(
|
||||
|
@ -58,7 +58,12 @@ class StateCardConfigurator extends LocalizeMixin(PolymerElement) {
|
||||
}
|
||||
|
||||
_localizeState(stateObj) {
|
||||
return computeStateDisplay(this.hass.localize, stateObj, this.hass.locale);
|
||||
return computeStateDisplay(
|
||||
this.hass.localize,
|
||||
stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("state-card-configurator", StateCardConfigurator);
|
||||
|
@ -52,7 +52,8 @@ export class StateCardDisplay extends LitElement {
|
||||
: computeStateDisplay(
|
||||
this.hass!.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -165,6 +165,7 @@ class StateCardInputNumber extends mixinBehaviors(
|
||||
this.hass.localize,
|
||||
newVal,
|
||||
this.hass.locale,
|
||||
this.hass.entities,
|
||||
newVal.state
|
||||
),
|
||||
mode: String(newVal.attributes.mode),
|
||||
|
@ -85,7 +85,12 @@ class StateCardMediaPlayer extends LocalizeMixin(PolymerElement) {
|
||||
computePrimaryText(localize, playerObj) {
|
||||
return (
|
||||
playerObj.primaryTitle ||
|
||||
computeStateDisplay(localize, playerObj.stateObj, this.hass.locale)
|
||||
computeStateDisplay(
|
||||
localize,
|
||||
playerObj.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -234,7 +234,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
category: Parameters<typeof getHassTranslations>[2],
|
||||
integration?: Parameters<typeof getHassTranslations>[3],
|
||||
configFlow?: Parameters<typeof getHassTranslations>[4],
|
||||
force = false
|
||||
force = true
|
||||
): Promise<LocalizeFunc> {
|
||||
if (
|
||||
__BACKWARDS_COMPAT__ &&
|
||||
|
@ -3011,10 +3011,12 @@
|
||||
"system_options": "System options",
|
||||
"documentation": "Documentation",
|
||||
"download_diagnostics": "Download diagnostics",
|
||||
"disable_debug_logging": "Disable debug logging",
|
||||
"known_issues": "Known issues",
|
||||
"delete": "Delete",
|
||||
"delete_confirm_title": "Delete {title}?",
|
||||
"delete_confirm_text": "Its devices and entities will be permanently deleted.",
|
||||
"enable_debug_logging": "Enable debug logging",
|
||||
"reload": "Reload",
|
||||
"restart_confirm": "Restart Home Assistant to finish removing this integration",
|
||||
"reload_confirm": "The integration was reloaded",
|
||||
@ -3049,6 +3051,7 @@
|
||||
"depends_on_cloud": "Depends on the cloud",
|
||||
"yaml_only": "Needs manual configuration",
|
||||
"disabled_polling": "Automatic polling for updated data disabled",
|
||||
"debug_logging_enabled": "Debug logging enabled",
|
||||
"state": {
|
||||
"loaded": "Loaded",
|
||||
"setup_error": "Failed to set up",
|
||||
|
@ -31,7 +31,7 @@ describe("computeStateDisplay", () => {
|
||||
attributes: {},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"component.binary_sensor.state._.off"
|
||||
);
|
||||
});
|
||||
@ -45,7 +45,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"component.binary_sensor.state.moisture.off"
|
||||
);
|
||||
});
|
||||
@ -65,7 +65,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"component.binary_sensor.state.invalid_device_class.off"
|
||||
);
|
||||
});
|
||||
@ -79,7 +79,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"123 m"
|
||||
);
|
||||
});
|
||||
@ -93,7 +93,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"1,234.5 m"
|
||||
);
|
||||
});
|
||||
@ -107,7 +107,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"1,234.5"
|
||||
);
|
||||
});
|
||||
@ -127,7 +127,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"state.default.unknown"
|
||||
);
|
||||
});
|
||||
@ -147,7 +147,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"state.default.unavailable"
|
||||
);
|
||||
});
|
||||
@ -165,7 +165,7 @@ describe("computeStateDisplay", () => {
|
||||
attributes: {},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"component.sensor.state._.custom_state"
|
||||
);
|
||||
});
|
||||
@ -187,14 +187,14 @@ describe("computeStateDisplay", () => {
|
||||
};
|
||||
it("Uses am/pm time format", () => {
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"November 18, 2017 at 11:12 PM"
|
||||
);
|
||||
});
|
||||
it("Uses 24h time format", () => {
|
||||
localeData.time_format = TimeFormat.twenty_four;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"November 18, 2017 at 23:12"
|
||||
);
|
||||
});
|
||||
@ -216,7 +216,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"November 18, 2017"
|
||||
);
|
||||
});
|
||||
@ -239,14 +239,14 @@ describe("computeStateDisplay", () => {
|
||||
it("Uses am/pm time format", () => {
|
||||
localeData.time_format = TimeFormat.am_pm;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"11:12 PM"
|
||||
);
|
||||
});
|
||||
it("Uses 24h time format", () => {
|
||||
localeData.time_format = TimeFormat.twenty_four;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}),
|
||||
"23:12"
|
||||
);
|
||||
});
|
||||
@ -273,6 +273,7 @@ describe("computeStateDisplay", () => {
|
||||
localize,
|
||||
stateObj,
|
||||
localeData,
|
||||
{},
|
||||
"2021-07-04 15:40:03"
|
||||
),
|
||||
"July 4, 2021 at 3:40 PM"
|
||||
@ -285,6 +286,7 @@ describe("computeStateDisplay", () => {
|
||||
localize,
|
||||
stateObj,
|
||||
localeData,
|
||||
{},
|
||||
"2021-07-04 15:40:03"
|
||||
),
|
||||
"July 4, 2021 at 15:40"
|
||||
@ -308,7 +310,7 @@ describe("computeStateDisplay", () => {
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData, "2021-07-04"),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}, "2021-07-04"),
|
||||
"July 4, 2021"
|
||||
);
|
||||
});
|
||||
@ -331,14 +333,14 @@ describe("computeStateDisplay", () => {
|
||||
it("Uses am/pm time format", () => {
|
||||
localeData.time_format = TimeFormat.am_pm;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData, "17:05:07"),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}, "17:05:07"),
|
||||
"5:05 PM"
|
||||
);
|
||||
});
|
||||
it("Uses 24h time format", () => {
|
||||
localeData.time_format = TimeFormat.twenty_four;
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData, "17:05:07"),
|
||||
computeStateDisplay(localize, stateObj, localeData, {}, "17:05:07"),
|
||||
"17:05"
|
||||
);
|
||||
});
|
||||
@ -357,7 +359,7 @@ describe("computeStateDisplay", () => {
|
||||
attributes: {},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"state.default.unavailable"
|
||||
);
|
||||
});
|
||||
@ -372,8 +374,26 @@ describe("computeStateDisplay", () => {
|
||||
attributes: {},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(altLocalize, stateObj, localeData),
|
||||
computeStateDisplay(altLocalize, stateObj, localeData, {}),
|
||||
"My Custom State"
|
||||
);
|
||||
});
|
||||
|
||||
it("Localizes using translation key", () => {
|
||||
const stateObj: any = {
|
||||
entity_id: "sensor.test",
|
||||
state: "custom_state",
|
||||
attributes: {},
|
||||
};
|
||||
const entities: any = {
|
||||
"sensor.test": {
|
||||
translation_key: "custom_translation",
|
||||
platform: "custom_integration",
|
||||
},
|
||||
};
|
||||
assert.strictEqual(
|
||||
computeStateDisplay(localize, stateObj, localeData, entities),
|
||||
"component.custom_integration.entity.sensor.custom_translation.state.custom_state"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user