20221201.0 (#14505)

This commit is contained in:
Bram Kragten 2022-12-01 17:13:52 +01:00 committed by GitHub
commit 77a53ffc6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 627 additions and 257 deletions

View File

@ -1,5 +1,5 @@
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs"); const fs = require("fs/promises");
const mapStream = require("map-stream"); const mapStream = require("map-stream");
const inDirFrontend = "translations/frontend"; const inDirFrontend = "translations/frontend";
@ -46,18 +46,21 @@ gulp.task("check-translations-html", function () {
return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml()); return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml());
}); });
gulp.task("check-all-files-exist", function () { gulp.task("check-all-files-exist", async function () {
const file = fs.readFileSync(srcMeta, { encoding }); const file = await fs.readFile(srcMeta, { encoding });
const meta = JSON.parse(file); const meta = JSON.parse(file);
const writings = [];
Object.keys(meta).forEach((lang) => { Object.keys(meta).forEach((lang) => {
if (!fs.existsSync(`${inDirFrontend}/${lang}.json`)) { writings.push(
fs.writeFileSync(`${inDirFrontend}/${lang}.json`, JSON.stringify({})); fs.writeFile(`${inDirFrontend}/${lang}.json`, JSON.stringify({}), {
} flag: "wx",
if (!fs.existsSync(`${inDirBackend}/${lang}.json`)) { }),
fs.writeFileSync(`${inDirBackend}/${lang}.json`, JSON.stringify({})); fs.writeFile(`${inDirBackend}/${lang}.json`, JSON.stringify({}), {
} flag: "wx",
})
);
}); });
return Promise.resolve(); await Promise.allSettled(writings);
}); });
gulp.task( gulp.task(

View File

@ -307,7 +307,8 @@ export class DemoEntityState extends LitElement {
html`${computeStateDisplay( html`${computeStateDisplay(
hass.localize, hass.localize,
entry.stateObj, entry.stateObj,
hass.locale hass.locale,
hass.entities
)}`, )}`,
}, },
device_class: { device_class: {

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20221130.0" version = "20221201.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -7,4 +7,4 @@ set -e
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
# Install node modules # Install node modules
yarn install yarn install

9
script/setup_translations Executable file
View 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

View File

@ -5,6 +5,7 @@ const ALERTING_DEVICE_CLASSES = new Set([
"carbon_monoxide", "carbon_monoxide",
"gas", "gas",
"heat", "heat",
"moisture",
"problem", "problem",
"safety", "safety",
"smoke", "smoke",

View File

@ -1,10 +1,12 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { EntityRegistryEntry } from "../../data/entity_registry";
import { FrontendLocaleData } from "../../data/translation"; import { FrontendLocaleData } from "../../data/translation";
import { import {
updateIsInstallingFromAttributes, updateIsInstallingFromAttributes,
UPDATE_SUPPORT_PROGRESS, UPDATE_SUPPORT_PROGRESS,
} from "../../data/update"; } from "../../data/update";
import { HomeAssistant } from "../../types";
import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration"; import { formatDuration, UNIT_TO_SECOND_CONVERT } from "../datetime/duration";
import { formatDate } from "../datetime/format_date"; import { formatDate } from "../datetime/format_date";
import { formatDateTime } from "../datetime/format_date_time"; import { formatDateTime } from "../datetime/format_date_time";
@ -23,11 +25,13 @@ export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
stateObj: HassEntity, stateObj: HassEntity,
locale: FrontendLocaleData, locale: FrontendLocaleData,
entities: HomeAssistant["entities"],
state?: string state?: string
): string => ): string =>
computeStateDisplayFromEntityAttributes( computeStateDisplayFromEntityAttributes(
localize, localize,
locale, locale,
entities,
stateObj.entity_id, stateObj.entity_id,
stateObj.attributes, stateObj.attributes,
state !== undefined ? state : stateObj.state state !== undefined ? state : stateObj.state
@ -36,6 +40,7 @@ export const computeStateDisplay = (
export const computeStateDisplayFromEntityAttributes = ( export const computeStateDisplayFromEntityAttributes = (
localize: LocalizeFunc, localize: LocalizeFunc,
locale: FrontendLocaleData, locale: FrontendLocaleData,
entities: HomeAssistant["entities"],
entityId: string, entityId: string,
attributes: any, attributes: any,
state: string state: string
@ -194,7 +199,13 @@ export const computeStateDisplayFromEntityAttributes = (
: localize("ui.card.update.up_to_date"); : localize("ui.card.update.up_to_date");
} }
const entity = entities[entityId] as EntityRegistryEntry | undefined;
return ( return (
(entity?.translation_key &&
localize(
`component.${entity.platform}.entity.${domain}.${entity.translation_key}.state.${state}`
)) ||
// Return device class translation // Return device class translation
(attributes.device_class && (attributes.device_class &&
localize( localize(

View File

@ -1,4 +1,7 @@
export const countries = [ import memoizeOne from "memoize-one";
import { caseInsensitiveStringCompare } from "../common/string/compare";
export const COUNTRIES = [
"AD", "AD",
"AE", "AE",
"AF", "AF",
@ -250,23 +253,31 @@ export const countries = [
"ZW", "ZW",
]; ];
export const countryDisplayNames = export const getCountryOptions = memoizeOne((language?: string) => {
Intl && "DisplayNames" in Intl const countryDisplayNames =
? new Intl.DisplayNames(undefined, { Intl && "DisplayNames" in Intl
type: "region", ? new Intl.DisplayNames(language, {
fallback: "code", type: "region",
}) fallback: "code",
: undefined; })
: 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 = () => { export const createCountryListEl = () => {
const list = document.createElement("datalist"); const list = document.createElement("datalist");
list.id = "countries"; list.id = "countries";
for (const country of countries) { const options = getCountryOptions();
for (const country of options) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = country; option.value = country.value;
option.innerText = countryDisplayNames option.innerText = country.label;
? countryDisplayNames.of(country)!
: country;
list.appendChild(option); list.appendChild(option);
} }
return list; return list;

View File

@ -1,4 +1,7 @@
export const currencies = [ import memoizeOne from "memoize-one";
import { caseInsensitiveStringCompare } from "../common/string/compare";
export const CURRENCIES = [
"AED", "AED",
"AFN", "AFN",
"ALL", "ALL",
@ -158,23 +161,29 @@ export const currencies = [
"ZWL", "ZWL",
]; ];
export const currencyDisplayNames = export const getCurrencyOptions = memoizeOne((language?: string) => {
Intl && "DisplayNames" in Intl const currencyDisplayNames =
? new Intl.DisplayNames(undefined, { Intl && "DisplayNames" in Intl
type: "currency", ? new Intl.DisplayNames(language, {
fallback: "code", type: "currency",
}) fallback: "code",
: undefined; })
: 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 = () => { export const createCurrencyListEl = () => {
const list = document.createElement("datalist"); const list = document.createElement("datalist");
list.id = "currencies"; list.id = "currencies";
for (const currency of currencies) { for (const currency of getCurrencyOptions()) {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = currency; option.value = currency.value;
option.innerText = currencyDisplayNames option.innerText = currency.label;
? currencyDisplayNames.of(currency)!
: currency;
list.appendChild(option); list.appendChild(option);
} }
return list; return list;

View File

@ -55,6 +55,7 @@ class HaEntityStatePicker extends LitElement {
this.hass.localize, this.hass.localize,
state, state,
this.hass.locale, this.hass.locale,
this.hass.entities,
key key
) )
: formatAttributeValue(this.hass, key), : formatAttributeValue(this.hass, key),

View File

@ -158,7 +158,8 @@ export class HaStateLabelBadge extends LitElement {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
entityState, entityState,
this.hass!.locale this.hass!.locale,
this.hass!.entities
); );
} }
} }

View File

@ -3,6 +3,7 @@ import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit"; import { ComboBoxLitRenderer, comboBoxRenderer } from "@vaadin/combo-box/lit";
import "@vaadin/combo-box/theme/material/vaadin-combo-box-light"; import "@vaadin/combo-box/theme/material/vaadin-combo-box-light";
import type { import type {
ComboBoxDataProvider,
ComboBoxLight, ComboBoxLight,
ComboBoxLightFilterChangedEvent, ComboBoxLightFilterChangedEvent,
ComboBoxLightOpenedChangedEvent, ComboBoxLightOpenedChangedEvent,
@ -82,6 +83,9 @@ export class HaComboBox extends LitElement {
@property({ attribute: false }) public filteredItems?: any[]; @property({ attribute: false }) public filteredItems?: any[];
@property({ attribute: false })
public dataProvider?: ComboBoxDataProvider<any>;
@property({ attribute: "allow-custom-value", type: Boolean }) @property({ attribute: "allow-custom-value", type: Boolean })
public allowCustomValue = false; public allowCustomValue = false;
@ -148,6 +152,7 @@ export class HaComboBox extends LitElement {
.items=${this.items} .items=${this.items}
.value=${this.value || ""} .value=${this.value || ""}
.filteredItems=${this.filteredItems} .filteredItems=${this.filteredItems}
.dataProvider=${this.dataProvider}
.allowCustomValue=${this.allowCustomValue} .allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required} .required=${this.required}
@ -225,13 +230,13 @@ export class HaComboBox extends LitElement {
} }
private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) { private _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
ev.stopPropagation();
const opened = ev.detail.value; const opened = ev.detail.value;
// delay this so we can handle click event for toggle button before setting _opened // delay this so we can handle click event for toggle button before setting _opened
setTimeout(() => { setTimeout(() => {
this.opened = opened; this.opened = opened;
}, 0); }, 0);
// @ts-ignore fireEvent(this, "opened-changed", { value: ev.detail.value });
fireEvent(this, ev.type, ev.detail);
if (opened) { if (opened) {
const overlay = document.querySelector<HTMLElement>( const overlay = document.querySelector<HTMLElement>(
@ -300,8 +305,8 @@ export class HaComboBox extends LitElement {
} }
private _filterChanged(ev: ComboBoxLightFilterChangedEvent) { private _filterChanged(ev: ComboBoxLightFilterChangedEvent) {
// @ts-ignore ev.stopPropagation();
fireEvent(this, ev.type, ev.detail, { composed: false }); fireEvent(this, "filter-changed", { value: ev.detail.value });
} }
private _valueChanged(ev: ComboBoxLightValueChangedEvent) { private _valueChanged(ev: ComboBoxLightValueChangedEvent) {
@ -363,3 +368,10 @@ declare global {
"ha-combo-box": HaComboBox; "ha-combo-box": HaComboBox;
} }
} }
declare global {
interface HASSDomEvents {
"filter-changed": { value: string };
"opened-changed": { value: boolean };
}
}

View File

@ -54,6 +54,7 @@ export class HaDateInput extends LitElement {
.disabled=${this.disabled} .disabled=${this.disabled}
iconTrailing iconTrailing
helperPersistent helperPersistent
readonly
@click=${this._openDialog} @click=${this._openDialog}
.value=${this.value .value=${this.value
? formatDateNumeric(new Date(this.value), this.locale) ? formatDateNumeric(new Date(this.value), this.locale)

View File

@ -1,28 +1,76 @@
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/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 { fireEvent } from "../common/dom/fire_event";
import { customIcons } from "../data/custom_icons"; import { customIcons } from "../data/custom_icons";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-combo-box"; import "./ha-combo-box";
import type { HaComboBox } from "./ha-combo-box";
import "./ha-icon"; import "./ha-icon";
type IconItem = { type IconItem = {
icon: string; icon: string;
parts: Set<string>;
keywords: string[]; keywords: string[];
}; };
let iconItems: IconItem[] = [];
let iconLoaded = false;
// eslint-disable-next-line lit/prefer-static-styles type RankedIcon = {
const rowRenderer: ComboBoxLitRenderer<IconItem> = (item) => html`<mwc-list-item icon: string;
graphic="avatar" rank: number;
> };
<ha-icon .icon=${item.icon} slot="graphic"></ha-icon>
${item.icon} let ICONS: IconItem[] = [];
</mwc-list-item>`; 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") @customElement("ha-icon-picker")
export class HaIconPicker extends LitElement { export class HaIconPicker extends LitElement {
@ -46,10 +94,6 @@ export class HaIconPicker extends LitElement {
@property({ type: Boolean }) public invalid = false; @property({ type: Boolean }) public invalid = false;
@state() private _opened = false;
@query("ha-combo-box", true) private comboBox!: HaComboBox;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<ha-combo-box <ha-combo-box
@ -58,7 +102,7 @@ export class HaIconPicker extends LitElement {
item-label-path="icon" item-label-path="icon"
.value=${this._value} .value=${this._value}
allow-custom-value allow-custom-value
.filteredItems=${iconItems} .dataProvider=${ICONS_LOADED ? this._iconProvider : undefined}
.label=${this.label} .label=${this.label}
.helper=${this.helper} .helper=${this.helper}
.disabled=${this.disabled} .disabled=${this.disabled}
@ -70,7 +114,6 @@ export class HaIconPicker extends LitElement {
icon icon
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
@filter-changed=${this._filterChanged}
> >
${this._value || this.placeholder ${this._value || this.placeholder
? html` ? html`
@ -87,46 +130,55 @@ export class HaIconPicker extends LitElement {
`; `;
} }
private async _openedChanged(ev: PolymerChangedEvent<boolean>) { // Filter can take a significant chunk of frame (up to 3-5 ms)
this._opened = ev.detail.value; private _filterIcons = memoizeOne(
if (this._opened && !iconLoaded) { (filter: string, iconItems: IconItem[] = ICONS) => {
const iconList = await import("../../build/mdi/iconList.json"); if (!filter) {
return iconItems;
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;
} }
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) { const filteredItems: RankedIcon[] = [];
return !this._opened || changedProps.has("_opened"); 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>) { 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() { private get _value() {
return this.value || ""; return this.value || "";
} }

View File

@ -85,7 +85,12 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) {
} }
_localizeState(stateObj) { _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); customElements.define("ha-water_heater-state", HaWaterHeaterState);

View File

@ -44,7 +44,7 @@ export class HaTileInfo extends LitElement {
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
letter-spacing: 0.4px; letter-spacing: 0.4px;
color: var(--secondary-text-color); color: var(--primary-text-color);
} }
`; `;
} }

View File

@ -21,6 +21,7 @@ export interface EntityRegistryEntry {
has_entity_name: boolean; has_entity_name: boolean;
original_name?: string; original_name?: string;
unique_id: string; unique_id: string;
translation_key?: string;
} }
export interface ExtEntityRegistryEntry extends EntityRegistryEntry { export interface ExtEntityRegistryEntry extends EntityRegistryEntry {

View File

@ -184,6 +184,7 @@ const equalState = (obj1: LineChartState, obj2: LineChartState) =>
const processTimelineEntity = ( const processTimelineEntity = (
localize: LocalizeFunc, localize: LocalizeFunc,
language: FrontendLocaleData, language: FrontendLocaleData,
entities: HomeAssistant["entities"],
entityId: string, entityId: string,
states: EntityHistoryState[], states: EntityHistoryState[],
current_state: HassEntity | undefined current_state: HassEntity | undefined
@ -198,6 +199,7 @@ const processTimelineEntity = (
state_localize: computeStateDisplayFromEntityAttributes( state_localize: computeStateDisplayFromEntityAttributes(
localize, localize,
language, language,
entities,
entityId, entityId,
state.a || first.a, state.a || first.a,
state.s state.s
@ -344,6 +346,7 @@ export const computeHistory = (
processTimelineEntity( processTimelineEntity(
localize, localize,
hass.locale, hass.locale,
hass.entities,
entityId, entityId,
stateInfo, stateInfo,
currentState currentState

View File

@ -1,5 +1,7 @@
import { Connection, createCollection } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export type IntegrationType = export type IntegrationType =
| "device" | "device"
@ -23,6 +25,7 @@ export interface IntegrationManifest {
zeroconf?: string[]; zeroconf?: string[];
homekit?: { models: string[] }; homekit?: { models: string[] };
integration_type?: IntegrationType; integration_type?: IntegrationType;
loggers?: string[];
quality_scale?: "gold" | "internal" | "platinum" | "silver"; quality_scale?: "gold" | "internal" | "platinum" | "silver";
iot_class: iot_class:
| "assumed_state" | "assumed_state"
@ -36,6 +39,24 @@ export interface IntegrationSetup {
seconds?: number; 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 = ( export const integrationIssuesUrl = (
domain: string, domain: string,
manifest: IntegrationManifest manifest: IntegrationManifest
@ -69,3 +90,46 @@ export const fetchIntegrationManifest = (
export const fetchIntegrationSetups = (hass: HomeAssistant) => export const fetchIntegrationSetups = (hass: HomeAssistant) =>
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" }); 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
);

View File

@ -435,7 +435,13 @@ export const localizeStateMessage = (
`${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`,
"state", "state",
stateObj stateObj
? computeStateDisplay(localize, stateObj, hass.locale, state) ? computeStateDisplay(
localize,
stateObj,
hass.locale,
hass.entities,
state
)
: state : state
); );
}; };

View File

@ -90,7 +90,12 @@ export const computeDisplayTimer = (
} }
if (stateObj.state === "idle" || timeRemaining === 0) { 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); let display = secondsToDuration(timeRemaining || 0);
@ -99,7 +104,8 @@ export const computeDisplayTimer = (
display = `${display} (${computeStateDisplay( display = `${display} (${computeStateDisplay(
hass.localize, hass.localize,
stateObj, stateObj,
hass.locale hass.locale,
hass.entities
)})`; )})`;
} }

View File

@ -44,6 +44,7 @@ declare global {
export type TranslationCategory = export type TranslationCategory =
| "title" | "title"
| "state" | "state"
| "entity"
| "config" | "config"
| "config_panel" | "config_panel"
| "options" | "options"

View File

@ -37,7 +37,8 @@ export class HuiConfiguratorNotificationItem extends LitElement {
>${computeStateDisplay( >${computeStateDisplay(
this.hass.localize, this.hass.localize,
this.notification, this.notification,
this.hass.locale this.hass.locale,
this.hass.entities
)}</mwc-button )}</mwc-button
> >
</notification-item-template> </notification-item-template>

View File

@ -307,7 +307,9 @@ export const provideHass = (
true true
); );
}, },
areas: {},
devices: {},
entities: {},
...overrideData, ...overrideData,
}; };

View File

@ -137,6 +137,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
super.hassConnected(); super.hassConnected();
// @ts-ignore // @ts-ignore
this._loadHassTranslations(this.hass!.language, "state"); this._loadHassTranslations(this.hass!.language, "state");
// @ts-ignore
this._loadHassTranslations(this.hass!.language, "entity");
document.addEventListener( document.addEventListener(
"visibilitychange", "visibilitychange",

View File

@ -1,6 +1,6 @@
import "@material/mwc-button"; import "@material/mwc-button";
import { mdiCalendarClock, mdiClose } from "@mdi/js"; 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 { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { RRule } from "rrule"; import { RRule } from "rrule";
@ -134,7 +134,10 @@ class DialogCalendarEventDetail extends LitElement {
private _formatDateRange() { private _formatDateRange() {
const start = new Date(this._data!.dtstart); 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. // The range can be shortened when the start and end are on the same day.
if (isSameDay(start, end)) { if (isSameDay(start, end)) {
if (isDate(this._data.dtstart)) { if (isDate(this._data.dtstart)) {
@ -148,10 +151,15 @@ class DialogCalendarEventDetail extends LitElement {
)} - ${formatTime(end, this.hass.locale)}`; )} - ${formatTime(end, this.hass.locale)}`;
} }
// An event across multiple dates, optionally with a time range // An event across multiple dates, optionally with a time range
return `${formatDateTime(start, this.hass.locale)} - ${formatDateTime( return `${
end, isDate(this._data.dtstart)
this.hass.locale ? 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() { private async _editEvent() {

View File

@ -4,6 +4,7 @@ import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { addDays, addHours, startOfHour } from "date-fns/esm"; import { addDays, addHours, startOfHour } from "date-fns/esm";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isDate } from "../../common/string/is_date"; import { isDate } from "../../common/string/is_date";
import "../../components/ha-date-input"; import "../../components/ha-date-input";
import "../../components/ha-time-input"; import "../../components/ha-time-input";
@ -39,7 +40,9 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _calendarId?: string; @state() private _calendarId?: string;
@state() private _data?: CalendarEventMutableParams; @state() private _summary = "";
@state() private _rrule?: string;
@state() private _allDay = false; @state() private _allDay = false;
@ -49,40 +52,30 @@ class DialogCalendarEventEditor extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
public async showDialog( public showDialog(params: CalendarEventEditDialogParams): void {
params: CalendarEventEditDialogParams
): Promise<void> {
this._error = undefined; this._error = undefined;
this._params = params; this._params = params;
this._calendars = params.calendars; this._calendars = params.calendars;
this._calendarId = params.calendarId || this._calendars[0].entity_id; this._calendarId = params.calendarId || this._calendars[0].entity_id;
if (params.entry) { if (params.entry) {
const entry = params.entry!; const entry = params.entry!;
this._data = entry;
this._allDay = isDate(entry.dtstart); this._allDay = isDate(entry.dtstart);
this._summary = entry.summary;
this._rrule = entry.rrule;
if (this._allDay) { if (this._allDay) {
this._dtstart = new Date(entry.dtstart); this._dtstart = new Date(entry.dtstart);
// Calendar event end dates are exclusive, but not shown that way in the UI. The // Calendar event end dates are exclusive, but not shown that way in the UI. The
// reverse happens when persisting the event. // reverse happens when persisting the event.
this._dtend = new Date(entry.dtend); this._dtend = addDays(new Date(entry.dtend), -1);
this._dtend.setDate(this._dtend.getDate() - 1);
} else { } else {
this._dtstart = new Date(entry.dtstart); this._dtstart = new Date(entry.dtstart);
this._dtend = new Date(entry.dtend); this._dtend = new Date(entry.dtend);
} }
} else { } else {
this._data = {
summary: "",
// Dates are set in _dateChanged()
dtstart: "",
dtend: "",
};
this._allDay = false; this._allDay = false;
this._dtstart = startOfHour(new Date()); this._dtstart = startOfHour(new Date());
this._dtend = addHours(this._dtstart, 1); this._dtend = addHours(this._dtstart, 1);
this._dateChanged();
} }
await this.updateComplete;
} }
protected render(): TemplateResult { protected render(): TemplateResult {
@ -90,6 +83,12 @@ class DialogCalendarEventEditor extends LitElement {
return html``; return html``;
} }
const isCreate = this._params.entry === undefined; const isCreate = this._params.entry === undefined;
const { startDate, startTime, endDate, endTime } = this._getLocaleStrings(
this._dtstart,
this._dtend
);
return html` return html`
<ha-dialog <ha-dialog
open open
@ -100,7 +99,7 @@ class DialogCalendarEventEditor extends LitElement {
<div class="header_title"> <div class="header_title">
${isCreate ${isCreate
? this.hass.localize("ui.components.calendar.event.add") ? this.hass.localize("ui.components.calendar.event.add")
: this._data!.summary} : this._summary}
</div> </div>
<ha-icon-button <ha-icon-button
.label=${this.hass.localize("ui.dialogs.generic.close")} .label=${this.hass.localize("ui.dialogs.generic.close")}
@ -155,13 +154,13 @@ class DialogCalendarEventEditor extends LitElement {
> >
<div class="flex"> <div class="flex">
<ha-date-input <ha-date-input
.value=${this._data!.dtstart} .value=${startDate}
.locale=${this.hass.locale} .locale=${this.hass.locale}
@value-changed=${this._startDateChanged} @value-changed=${this._startDateChanged}
></ha-date-input> ></ha-date-input>
${!this._allDay ${!this._allDay
? html`<ha-time-input ? html`<ha-time-input
.value=${this._data!.dtstart.split("T")[1]} .value=${startTime}
.locale=${this.hass.locale} .locale=${this.hass.locale}
@value-changed=${this._startTimeChanged} @value-changed=${this._startTimeChanged}
></ha-time-input>` ></ha-time-input>`
@ -174,14 +173,14 @@ class DialogCalendarEventEditor extends LitElement {
> >
<div class="flex"> <div class="flex">
<ha-date-input <ha-date-input
.value=${this._data!.dtend} .value=${endDate}
.min=${this._data!.dtstart} .min=${startDate}
.locale=${this.hass.locale} .locale=${this.hass.locale}
@value-changed=${this._endDateChanged} @value-changed=${this._endDateChanged}
></ha-date-input> ></ha-date-input>
${!this._allDay ${!this._allDay
? html`<ha-time-input ? html`<ha-time-input
.value=${this._data!.dtend.split("T")[1]} .value=${endTime}
.locale=${this.hass.locale} .locale=${this.hass.locale}
@value-changed=${this._endTimeChanged} @value-changed=${this._endTimeChanged}
></ha-time-input>` ></ha-time-input>`
@ -190,7 +189,7 @@ class DialogCalendarEventEditor extends LitElement {
</div> </div>
<ha-recurrence-rule-editor <ha-recurrence-rule-editor
.locale=${this.hass.locale} .locale=${this.hass.locale}
.value=${this._data!.rrule || ""} .value=${this._rrule || ""}
@value-changed=${this._handleRRuleChanged} @value-changed=${this._handleRRuleChanged}
> >
</ha-recurrence-rule-editor> </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) { private _handleSummaryChanged(ev) {
this._data!.summary = ev.target.value; this._summary = ev.target.value;
} }
private _handleRRuleChanged(ev) { private _handleRRuleChanged(ev) {
this._data!.rrule = ev.detail.value; this._rrule = ev.detail.value;
this.requestUpdate();
} }
private _allDayToggleChanged(ev) { private _allDayToggleChanged(ev) {
this._allDay = ev.target.checked; this._allDay = ev.target.checked;
this._dateChanged();
} }
private _startDateChanged(ev: CustomEvent) { private _startDateChanged(ev: CustomEvent) {
this._dtstart = new Date( this._dtstart = new Date(
ev.detail.value + "T" + this._dtstart!.toISOString().split("T")[1] ev.detail.value + "T" + this._dtstart!.toISOString().split("T")[1]
); );
this._dateChanged();
} }
private _endDateChanged(ev: CustomEvent) { private _endDateChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = new Date(
ev.detail.value + "T" + this._dtend!.toISOString().split("T")[1] ev.detail.value + "T" + this._dtend!.toISOString().split("T")[1]
); );
this._dateChanged();
} }
private _startTimeChanged(ev: CustomEvent) { private _startTimeChanged(ev: CustomEvent) {
this._dtstart = new Date( this._dtstart = new Date(
this._dtstart!.toISOString().split("T")[0] + "T" + ev.detail.value this._dtstart!.toISOString().split("T")[0] + "T" + ev.detail.value
); );
this._dateChanged();
} }
private _endTimeChanged(ev: CustomEvent) { private _endTimeChanged(ev: CustomEvent) {
this._dtend = new Date( this._dtend = new Date(
this._dtend!.toISOString().split("T")[0] + "T" + ev.detail.value 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) { if (this._allDay) {
this._data!.dtstart = this._dtstart!.toISOString(); data.dtstart = startDate!;
// End date/time is exclusive when persisted // 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 { } else {
this._data!.dtstart = this._dtstart!.toISOString(); data.dtstart = `${startDate}T${startTime}`;
this._data!.dtend = this._dtend!.toISOString(); data.dtend = `${endDate}T${endTime}`;
} }
return data;
} }
private _handleCalendarChanged(ev: CustomEvent) { private _handleCalendarChanged(ev: CustomEvent) {
@ -290,7 +310,11 @@ class DialogCalendarEventEditor extends LitElement {
private async _createEvent() { private async _createEvent() {
this._submitting = true; this._submitting = true;
try { try {
await createCalendarEvent(this.hass!, this._calendarId!, this._data!); await createCalendarEvent(
this.hass!,
this._calendarId!,
this._calculateData()
);
} catch (err: any) { } catch (err: any) {
this._error = err ? err.message : "Unknown error"; this._error = err ? err.message : "Unknown error";
} finally { } finally {
@ -358,9 +382,10 @@ class DialogCalendarEventEditor extends LitElement {
this._calendars = []; this._calendars = [];
this._calendarId = undefined; this._calendarId = undefined;
this._params = undefined; this._params = undefined;
this._data = undefined;
this._dtstart = undefined; this._dtstart = undefined;
this._dtend = undefined; this._dtend = undefined;
this._summary = "";
this._rrule = undefined;
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -53,19 +53,22 @@ export class RecurrenceRuleEditor extends LitElement {
protected willUpdate(changedProps: PropertyValues) { protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps); 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; return;
} }
this._interval = 1; this._interval = 1;
this._weekday.clear(); this._weekday.clear();
this._end = "never"; this._end = "never";
this._count = undefined; this._count = undefined;
this._until = undefined; this._until = undefined;
this._allWeekdays = getWeekdays(firstWeekdayIndex(this.locale)).map(
(day: Weekday) => day.toString() as WeekdayStr
);
this._computedRRule = this.value; this._computedRRule = this.value;
if (this.value === "") { if (this.value === "") {
this._freq = "none"; this._freq = "none";
@ -274,6 +277,7 @@ export class RecurrenceRuleEditor extends LitElement {
} }
private _onUntilChange(e: CustomEvent) { private _onUntilChange(e: CustomEvent) {
e.stopPropagation();
this._until = new Date(e.detail.value); this._until = new Date(e.detail.value);
this._updateRule(); this._updateRule();
} }

View File

@ -2,6 +2,7 @@
// and the values defined by rrule.js. // and the values defined by rrule.js.
import { RRule, Frequency, Weekday } from "rrule"; import { RRule, Frequency, Weekday } from "rrule";
import type { WeekdayStr } from "rrule"; import type { WeekdayStr } from "rrule";
import { addDays, addMonths, addWeeks, addYears } from "date-fns";
export type RepeatFrequency = export type RepeatFrequency =
| "none" | "none"
@ -35,14 +36,14 @@ export function untilValue(freq: RepeatFrequency): Date {
const increment = DEFAULT_COUNT[freq]; const increment = DEFAULT_COUNT[freq];
switch (freq) { switch (freq) {
case "yearly": case "yearly":
return new Date(new Date().setFullYear(today.getFullYear() + increment)); return addYears(today, increment);
case "monthly": case "monthly":
return new Date(new Date().setMonth(today.getMonth() + increment)); return addMonths(today, increment);
case "weekly": case "weekly":
return new Date(new Date().setDate(today.getDate() + 7 * increment)); return addWeeks(today, increment);
case "daily": case "daily":
default: default:
return new Date(new Date().setDate(today.getDate() + increment)); return addDays(today, increment);
} }
} }

View File

@ -6,16 +6,11 @@ import memoizeOne from "memoize-one";
import { UNIT_C } from "../../../common/const"; import { UNIT_C } from "../../../common/const";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { navigate } from "../../../common/navigate"; import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/buttons/ha-progress-button"; import "../../../components/buttons/ha-progress-button";
import type { HaProgressButton } from "../../../components/buttons/ha-progress-button"; import type { HaProgressButton } from "../../../components/buttons/ha-progress-button";
import { import { getCountryOptions } from "../../../components/country-datalist";
countries, import { getCurrencyOptions } from "../../../components/currency-datalist";
countryDisplayNames,
} from "../../../components/country-datalist";
import {
currencies,
currencyDisplayNames,
} from "../../../components/currency-datalist";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-formfield"; import "../../../components/ha-formfield";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
@ -55,6 +50,8 @@ class HaConfigSectionGeneral extends LitElement {
@state() private _location?: [number, number]; @state() private _location?: [number, number];
@state() private _languages?: { value: string; label: string }[];
protected render(): TemplateResult { protected render(): TemplateResult {
const canEdit = ["storage", "default"].includes( const canEdit = ["storage", "default"].includes(
this.hass.config.config_source this.hass.config.config_source
@ -187,13 +184,11 @@ class HaConfigSectionGeneral extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
@change=${this._handleChange} @change=${this._handleChange}
> >
${currencies.map( ${getCurrencyOptions(this.hass.locale.language).map(
(currency) => ({ value, label }) =>
html`<mwc-list-item .value=${currency} html`<mwc-list-item .value=${value}>
>${currencyDisplayNames ${label}
? currencyDisplayNames.of(currency) </mwc-list-item>`
: currency}</mwc-list-item
>`
)}</ha-select )}</ha-select
> >
<a <a
@ -218,13 +213,11 @@ class HaConfigSectionGeneral extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
@change=${this._handleChange} @change=${this._handleChange}
> >
${countries.map( ${getCountryOptions(this.hass.locale.language).map(
(country) => ({ value, label }) =>
html`<mwc-list-item .value=${country} html`<mwc-list-item .value=${value}>
>${countryDisplayNames ${label}
? countryDisplayNames.of(country) </mwc-list-item>`
: country}</mwc-list-item
>`
)}</ha-select )}</ha-select
> >
<ha-select <ha-select
@ -239,12 +232,10 @@ class HaConfigSectionGeneral extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
@change=${this._handleChange} @change=${this._handleChange}
> >
${Object.entries( ${this._languages?.map(
this.hass.translationMetadata.translations ({ value, label }) =>
).map( html`<mwc-list-item .value=${value}
([code, metadata]) => >${label}</mwc-list-item
html`<mwc-list-item .value=${code}
>${metadata.nativeName}</mwc-list-item
>` >`
)}</ha-select )}</ha-select
> >
@ -300,6 +291,21 @@ class HaConfigSectionGeneral extends LitElement {
this._elevation = this.hass.config.elevation; this._elevation = this.hass.config.elevation;
this._timeZone = this.hass.config.time_zone; this._timeZone = this.hass.config.time_zone;
this._name = this.hass.config.location_name; 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) { private _handleChange(ev) {

View File

@ -51,6 +51,8 @@ import {
fetchIntegrationManifest, fetchIntegrationManifest,
fetchIntegrationManifests, fetchIntegrationManifests,
IntegrationManifest, IntegrationManifest,
IntegrationLogInfo,
subscribeLogInfo,
} from "../../../data/integration"; } from "../../../data/integration";
import { import {
getIntegrationDescriptions, getIntegrationDescriptions,
@ -154,6 +156,10 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
@state() private _diagnosticHandlers?: Record<string, boolean>; @state() private _diagnosticHandlers?: Record<string, boolean>;
@state() private _logInfos?: {
[integration: string]: IntegrationLogInfo;
};
public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> { public hassSubscribe(): Array<UnsubscribeFunc | Promise<UnsubscribeFunc>> {
return [ return [
subscribeEntityRegistry(this.hass.connection, (entries) => { subscribeEntityRegistry(this.hass.connection, (entries) => {
@ -230,6 +236,13 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}, },
{ type: ["device", "hub", "service"] } { 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 .supportsDiagnostics=${this._diagnosticHandlers
? this._diagnosticHandlers[domain] ? this._diagnosticHandlers[domain]
: false} : false}
.logInfo=${this._logInfos ? this._logInfos[domain] : null}
></ha-integration-card>` ></ha-integration-card>`
) )
: this._filter && : this._filter &&

View File

@ -5,6 +5,8 @@ import {
mdiAlertCircle, mdiAlertCircle,
mdiBookshelf, mdiBookshelf,
mdiBug, mdiBug,
mdiBugPlay,
mdiBugStop,
mdiChevronLeft, mdiChevronLeft,
mdiCog, mdiCog,
mdiDelete, mdiDelete,
@ -47,11 +49,17 @@ import {
ERROR_STATES, ERROR_STATES,
RECOVERABLE_STATES, RECOVERABLE_STATES,
} from "../../../data/config_entries"; } from "../../../data/config_entries";
import { getErrorLogDownloadUrl } from "../../../data/error_log";
import type { DeviceRegistryEntry } from "../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../data/device_registry";
import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics"; import { getConfigEntryDiagnosticsDownloadUrl } from "../../../data/diagnostics";
import type { EntityRegistryEntry } from "../../../data/entity_registry"; import type { EntityRegistryEntry } from "../../../data/entity_registry";
import type { IntegrationManifest } from "../../../data/integration"; 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 { showConfigEntrySystemOptionsDialog } from "../../../dialogs/config-entry-system-options/show-dialog-config-entry-system-options";
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow"; import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
import { import {
@ -95,6 +103,8 @@ export class HaIntegrationCard extends LitElement {
@property({ type: Boolean }) public supportsDiagnostics = false; @property({ type: Boolean }) public supportsDiagnostics = false;
@property() public logInfo?: IntegrationLogInfo;
protected render(): TemplateResult { protected render(): TemplateResult {
let item = this._selectededConfigEntry; let item = this._selectededConfigEntry;
@ -137,6 +147,8 @@ export class HaIntegrationCard extends LitElement {
.localizedDomainName=${item ? item.localized_domain_name : undefined} .localizedDomainName=${item ? item.localized_domain_name : undefined}
.manifest=${this.manifest} .manifest=${this.manifest}
.configEntry=${item} .configEntry=${item}
.debugLoggingEnabled=${this.logInfo &&
this.logInfo.level === LogSeverity.DEBUG}
> >
${this.items.length > 1 ${this.items.length > 1
? html` ? html`
@ -398,6 +410,28 @@ export class HaIntegrationCard extends LitElement {
</mwc-list-item> </mwc-list-item>
</a>` </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 &&
(this.manifest.is_built_in || (this.manifest.is_built_in ||
this.manifest.issue_tracker || 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 { private get _selectededConfigEntry(): ConfigEntryExtended | undefined {
return this.items.length === 1 return this.items.length === 1
? this.items[0] ? this.items[0]

View File

@ -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 "@polymer/paper-tooltip/paper-tooltip";
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators"; 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 configEntry?: ConfigEntry;
@property({ attribute: false }) public debugLoggingEnabled?: boolean;
protected render(): TemplateResult { protected render(): TemplateResult {
let primary: string; let primary: string;
let secondary: string | undefined; 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` return html`
${!this.banner ? "" : html`<div class="banner">${this.banner}</div>`} ${!this.banner ? "" : html`<div class="banner">${this.banner}</div>`}
<slot name="above-header"></slot> <slot name="above-header"></slot>

View File

@ -205,7 +205,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</span>` </span>`
: ""} : ""}

View File

@ -167,7 +167,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
: computeStateDisplay( : computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)}</span )}</span
>${showUnit >${showUnit
? html` ? html`

View File

@ -335,7 +335,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.locale this.hass!.locale,
this.hass!.entities
)} )}
</div> </div>
` `

View File

@ -160,7 +160,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</div> </div>
` `

View File

@ -121,7 +121,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
const entityState = computeStateDisplay( const entityState = computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
); );
let footer: TemplateResult | string = ""; let footer: TemplateResult | string = "";

View File

@ -255,7 +255,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
title=${`${computeStateName(stateObj)} : ${computeStateDisplay( title=${`${computeStateName(stateObj)} : ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.locale this.hass!.locale,
this.hass!.entities
)}`} )}`}
> >
<ha-state-icon <ha-state-icon
@ -277,7 +278,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.locale this.hass!.locale,
this.hass!.entities
)} )}
</div> </div>
`} `}

View File

@ -201,7 +201,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return computeStateDisplay( return computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.locale this.hass!.locale,
this.hass!.entities
); );
} }

View File

@ -221,7 +221,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${computeStateDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</div> </div>
<div class="name" .title=${name}>${name}</div> <div class="name" .title=${name}>${name}</div>

View File

@ -83,7 +83,12 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
)} )}
> >
${this._config.prefix}${!this._config.attribute ${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} : stateObj.attributes[this._config.attribute]}${this._config.suffix}
</div> </div>
`; `;

View File

@ -69,7 +69,8 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow {
${computeStateDisplay( ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</div> </div>
`} `}

View File

@ -100,6 +100,7 @@ class HuiInputNumberEntityRow extends LitElement implements LovelaceRow {
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale, this.hass.locale,
this.hass.entities,
stateObj.state stateObj.state
)} )}
</span> </span>

View File

@ -193,7 +193,12 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
.hass=${this.hass} .hass=${this.hass}
.config=${this._config} .config=${this._config}
.secondaryText=${mediaDescription || .secondaryText=${mediaDescription ||
computeStateDisplay(this.hass.localize, stateObj, this.hass.locale)} computeStateDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.entities
)}
> >
<div class="controls"> <div class="controls">
${supportsFeature(stateObj, SUPPORT_TURN_ON) && ${supportsFeature(stateObj, SUPPORT_TURN_ON) &&

View File

@ -104,6 +104,7 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow {
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale, this.hass.locale,
this.hass.entities,
stateObj.state stateObj.state
)} )}
</span> </span>

View File

@ -83,7 +83,8 @@ class HuiSensorEntityRow extends LitElement implements LovelaceRow {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</div> </div>
</hui-generic-entity-row> </hui-generic-entity-row>

View File

@ -49,7 +49,12 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
return html` return html`
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}> <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> </hui-generic-entity-row>
`; `;
} }

View File

@ -64,7 +64,8 @@ class HuiToggleEntityRow extends LitElement implements LovelaceRow {
${computeStateDisplay( ${computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,
this.hass!.locale this.hass!.locale,
this.hass!.entities
)} )}
</div> </div>
`} `}

View File

@ -120,7 +120,8 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
? computeStateDisplay( ? computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
) )
: html` : html`
${formatNumber( ${formatNumber(

View File

@ -58,7 +58,12 @@ class StateCardConfigurator extends LocalizeMixin(PolymerElement) {
} }
_localizeState(stateObj) { _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); customElements.define("state-card-configurator", StateCardConfigurator);

View File

@ -52,7 +52,8 @@ export class StateCardDisplay extends LitElement {
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
this.stateObj, this.stateObj,
this.hass.locale this.hass.locale,
this.hass.entities
)} )}
</div> </div>
</div> </div>

View File

@ -165,6 +165,7 @@ class StateCardInputNumber extends mixinBehaviors(
this.hass.localize, this.hass.localize,
newVal, newVal,
this.hass.locale, this.hass.locale,
this.hass.entities,
newVal.state newVal.state
), ),
mode: String(newVal.attributes.mode), mode: String(newVal.attributes.mode),

View File

@ -85,7 +85,12 @@ class StateCardMediaPlayer extends LocalizeMixin(PolymerElement) {
computePrimaryText(localize, playerObj) { computePrimaryText(localize, playerObj) {
return ( return (
playerObj.primaryTitle || playerObj.primaryTitle ||
computeStateDisplay(localize, playerObj.stateObj, this.hass.locale) computeStateDisplay(
localize,
playerObj.stateObj,
this.hass.locale,
this.hass.entities
)
); );
} }
} }

View File

@ -234,7 +234,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
category: Parameters<typeof getHassTranslations>[2], category: Parameters<typeof getHassTranslations>[2],
integration?: Parameters<typeof getHassTranslations>[3], integration?: Parameters<typeof getHassTranslations>[3],
configFlow?: Parameters<typeof getHassTranslations>[4], configFlow?: Parameters<typeof getHassTranslations>[4],
force = false force = true
): Promise<LocalizeFunc> { ): Promise<LocalizeFunc> {
if ( if (
__BACKWARDS_COMPAT__ && __BACKWARDS_COMPAT__ &&

View File

@ -3011,10 +3011,12 @@
"system_options": "System options", "system_options": "System options",
"documentation": "Documentation", "documentation": "Documentation",
"download_diagnostics": "Download diagnostics", "download_diagnostics": "Download diagnostics",
"disable_debug_logging": "Disable debug logging",
"known_issues": "Known issues", "known_issues": "Known issues",
"delete": "Delete", "delete": "Delete",
"delete_confirm_title": "Delete {title}?", "delete_confirm_title": "Delete {title}?",
"delete_confirm_text": "Its devices and entities will be permanently deleted.", "delete_confirm_text": "Its devices and entities will be permanently deleted.",
"enable_debug_logging": "Enable debug logging",
"reload": "Reload", "reload": "Reload",
"restart_confirm": "Restart Home Assistant to finish removing this integration", "restart_confirm": "Restart Home Assistant to finish removing this integration",
"reload_confirm": "The integration was reloaded", "reload_confirm": "The integration was reloaded",
@ -3049,6 +3051,7 @@
"depends_on_cloud": "Depends on the cloud", "depends_on_cloud": "Depends on the cloud",
"yaml_only": "Needs manual configuration", "yaml_only": "Needs manual configuration",
"disabled_polling": "Automatic polling for updated data disabled", "disabled_polling": "Automatic polling for updated data disabled",
"debug_logging_enabled": "Debug logging enabled",
"state": { "state": {
"loaded": "Loaded", "loaded": "Loaded",
"setup_error": "Failed to set up", "setup_error": "Failed to set up",

View File

@ -31,7 +31,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"component.binary_sensor.state._.off" "component.binary_sensor.state._.off"
); );
}); });
@ -45,7 +45,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"component.binary_sensor.state.moisture.off" "component.binary_sensor.state.moisture.off"
); );
}); });
@ -65,7 +65,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"component.binary_sensor.state.invalid_device_class.off" "component.binary_sensor.state.invalid_device_class.off"
); );
}); });
@ -79,7 +79,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"123 m" "123 m"
); );
}); });
@ -93,7 +93,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"1,234.5 m" "1,234.5 m"
); );
}); });
@ -107,7 +107,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"1,234.5" "1,234.5"
); );
}); });
@ -127,7 +127,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"state.default.unknown" "state.default.unknown"
); );
}); });
@ -147,7 +147,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -165,7 +165,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"component.sensor.state._.custom_state" "component.sensor.state._.custom_state"
); );
}); });
@ -187,14 +187,14 @@ describe("computeStateDisplay", () => {
}; };
it("Uses am/pm time format", () => { it("Uses am/pm time format", () => {
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"November 18, 2017 at 11:12 PM" "November 18, 2017 at 11:12 PM"
); );
}); });
it("Uses 24h time format", () => { it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four; localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"November 18, 2017 at 23:12" "November 18, 2017 at 23:12"
); );
}); });
@ -216,7 +216,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"November 18, 2017" "November 18, 2017"
); );
}); });
@ -239,14 +239,14 @@ describe("computeStateDisplay", () => {
it("Uses am/pm time format", () => { it("Uses am/pm time format", () => {
localeData.time_format = TimeFormat.am_pm; localeData.time_format = TimeFormat.am_pm;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"11:12 PM" "11:12 PM"
); );
}); });
it("Uses 24h time format", () => { it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four; localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData), computeStateDisplay(localize, stateObj, localeData, {}),
"23:12" "23:12"
); );
}); });
@ -273,6 +273,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
{},
"2021-07-04 15:40:03" "2021-07-04 15:40:03"
), ),
"July 4, 2021 at 3:40 PM" "July 4, 2021 at 3:40 PM"
@ -285,6 +286,7 @@ describe("computeStateDisplay", () => {
localize, localize,
stateObj, stateObj,
localeData, localeData,
{},
"2021-07-04 15:40:03" "2021-07-04 15:40:03"
), ),
"July 4, 2021 at 15:40" "July 4, 2021 at 15:40"
@ -308,7 +310,7 @@ describe("computeStateDisplay", () => {
}, },
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, "2021-07-04"), computeStateDisplay(localize, stateObj, localeData, {}, "2021-07-04"),
"July 4, 2021" "July 4, 2021"
); );
}); });
@ -331,14 +333,14 @@ describe("computeStateDisplay", () => {
it("Uses am/pm time format", () => { it("Uses am/pm time format", () => {
localeData.time_format = TimeFormat.am_pm; localeData.time_format = TimeFormat.am_pm;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, "17:05:07"), computeStateDisplay(localize, stateObj, localeData, {}, "17:05:07"),
"5:05 PM" "5:05 PM"
); );
}); });
it("Uses 24h time format", () => { it("Uses 24h time format", () => {
localeData.time_format = TimeFormat.twenty_four; localeData.time_format = TimeFormat.twenty_four;
assert.strictEqual( assert.strictEqual(
computeStateDisplay(localize, stateObj, localeData, "17:05:07"), computeStateDisplay(localize, stateObj, localeData, {}, "17:05:07"),
"17:05" "17:05"
); );
}); });
@ -357,7 +359,7 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"state.default.unavailable" "state.default.unavailable"
); );
}); });
@ -372,8 +374,26 @@ describe("computeStateDisplay", () => {
attributes: {}, attributes: {},
}; };
assert.strictEqual( assert.strictEqual(
computeStateDisplay(altLocalize, stateObj, localeData), computeStateDisplay(altLocalize, stateObj, localeData, {}),
"My Custom State" "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"
);
});
}); });